5.1.1 Linux 异常处理的层次结构
-
异常的作用
异常,就是可以打断CPU正常运行流程的一些事情,如外部中断、未定义指令中断、试图修改只读的数据、执行swi指令(Software Interrupt Instruction)等。当异常发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。
- 未定义指令异常,操作系统可以利用其来使用一些自定义的机器指令,它们在异常处理函数中实现。
- 数据访问中止异常,可以将一块数据设为只读,然后提供给多个进程共用,这压根可以节省内存。当某个进程试图修改其中的数据时,将出发“数据访问中止异常”,在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
- 当用户程序试图读写的数据或执行的指令不在内存中时,也会出发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存, 还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
- 当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址对齐的问题。
- 用户程序可以通过 "swi" 指令触发 "swi异常",操作系统在 swi 异常处理函数中实现各种系统调用。
-
Linux 内核对异常的设置
内核在
start_kernel()
函数中调用trap_init
、init_IRQ
这两个函数来设置异常的处理函数。init/main.c: asmlinkage void __init start_kernel(void) { ... trap_init(); init_IRQ(); ... }
(1)
trap_init()
函数分析(linux-2.6.22.6\arch\arm\kernel\traps.c)该函数用来设置各种异常的处理向量,包括中断向量。“向量”就是被放在固定位置的代码,当发生异常时,CPU会自动执行这些固定位置上的指令。ARM架构CPU的异常向量基址可以是
0x00000000
或0xFFFF0000
,Linux内核使用0xFFFF0000
。trap_init()
函数就是将异常向量复制到基质处,代码如下:
721行中,vectors = CONFIG_VECTORS_BASE
,CONFIG_VECTORS_BASE
是一个配置项(内核配置选项),在Linux顶层目录下,文件.config
,搜索CONFIG_VECTORS_BASE
故vectors = CONFIG_VECTORS_BASE = 0xffff0000
。而地址 __vectors_start, __vectors_end
之间的代码就是异常向量,在arch/arm/kernel/entry-armv.S
中定义,它们被复制到地址 0xFFFF0000
处。
异常向量的代码,大部分都是一些跳转指令。发生异常时,CPU自动执行这些指令,然后再跳转去执行更复杂的代码,比如保存被中断的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行程序。这部分”复杂代码“在地址__stubs_start, __stubs_end
之间,它们在arch/arm/kernel/entry-armv.S
中定义,第722行代码将其复制到地址0xFFFF0000+Ox200
处。
异常向量的代码如下,其中的stubs_offset
用来重新定位跳转的位置。
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0 /* 复位时,CPU将执行这条指令 */
b vector_und + stubs_offset /* 未定义指令异常时,CPU执行该指令 */
ldr pc, .LCvswi + stubs_offset /* swi异常 */
b vector_pabt + stubs_offset /* 指令预取中止 */
b vector_dabt + stubs_offset /* 数据访问中止 */
b vector_addrexcptn + stubs_offset /* 没有用到 */
b vector_irq + stubs_offset /* irq异常 */
b vector_fiq + stubs_offset /* fiq异常 */
.globl __vectors_end
__vectors_end:
stubs_offset
的确定:
当汇编器看到 B 指令,把要跳转的标签转化为相对于当前PC的偏移量(±32 M )写入指令码。由于内核启动时中断向量表和 stubs 都发生了代码便宜,所以如果中断向量表中仍然写成b vector_irq
,那么实际执行的时候就无法跳转到搬移后的vector_irq
处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成偏移后的。设偏移后的偏移量为offset
,则:offset = L1 + L2 = [0x200 - (irq_PC_x - _vector_start_x)] + (vector_irq_x - _stubs_start_x) = 0x200 - irq_PC + _vector_start + vector_irq - _stubs_start = vector_irq + (_vector_start + 0x200 - _stubs_start) - irq_PC 令: stubs_offset = _vector_start + 0x200 - _stubs_start 则: offset = vector_irq + stubs_offset - irq_PC 所以中断入口点的跳转指令为: b vector_irq + stubs_offset 其中"- irq_PC"是由汇编器在编译时完成的
其中的vector_und
、vector_pabt
等表示要跳转去执行的代码。以vector_und
为例,仍是在该文件中,通过 vector_stub
宏来定义,代码如下:
/*
* Undef instr entry dispatcher
* Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*/
vector_stub und, UND_MODE
.long __und_usr @ 0 (USR_26 / USR_32),在用户模式执行了未定义指令
.long __und_invalid @ 1 (FIQ_26 / FIQ_32)
.long __und_invalid @ 2 (IRQ_26 / IRQ_32)
.long __und_svc @ 3 (SVC_26 / SVC_32)
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f
.align 5
这段代码表示在各个工作模式下执行未定义指令时,发生的异常的处理分支。如
__und_usr
表示在用户模式下, 执行未定义指令时,所发生的未定义异常将由它来处理;__und_svc
表示在管理模式下执行未定义指令时,所发生的未定义异常由它来处理。在其它工作模式下不可能发生未定义指令异常,否则使用__und_invalid
来处理错误。ARM架构CPU中使用4位数据来表示工作模式(目前只有7中工作模式),所以共有16个跳转分支。
vector_stub
是一个宏,它根据后面的参数und
、UND_MODE
定义了以vector_und
为标号的一段代码。这个宏为:
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
.if \correction
sub lr, lr, #\correction
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
.endm
按参数展开为:
.macro vector_stub, name, mode, correction=0
.align 5
vector_und:
.if 0
sub lr, lr, #0
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(UND_MODE ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
.endm
这个vector_stub
宏的功能是:计算处理完异常后的返回地址、保存一些寄存器(如r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式调用相应的跳转分支(如.long __und_usr
)。当发生异常时,CPU会根据异常的类型进入某个工作模式,但是很快 vector_stub
宏又会强制CPU进入管理模式,在管理模式下进行后续处理,这种方法简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。
不同的跳转分支(__und_usr
、__und_svc
)只是在它们的入口处(比如保存被中断成都的寄存器)稍有差别,后续的处理大致相同,都是调用相应的C函数。比如未定义指令异常发生时,最终会调用do_undefinstr
来进行处理。
各种异常的C处理函数可以分为5类,分布在不同的文件中:
arch/arm/kernel/traps.c
未定义指令异常的C处理函数,do_undefinstr
arch/arm/mm/fault.c
于内存相关访问异常的C处理函数,do_DataAbort
、do_PrefetchAbort
arch/arm/kernel/irq.c
中断处理函数的在这个文件夹中定义,总入口函数asm_do_IRQ
,它调用其它文件注册的中断处理函数
arch/arm/kernel/calls.S
swi 异常的处理函数指针被组织成一个表格:swi 指令机器码的位[23:0]被用来作为索引。通过不同的swi index
指令调用不同的 swi 异常处理函数,被称为系统调用,如sys_open
、sys_read
、sys_write
等。没有使用的异常
在Linux 2.6.22.6 中没有使用FIQ异常
trap_init()
函数搭建了各类异常的处理框架。当发生异常时,各种C处理函数会被调用。这个C函数还要进一步细分异常发生的情况, 分别调用更具体的处理函数。
(2) init_IRQ()
函数分析
中断也是一种异常,单独提出来是因为中断的处理于具体开发板密切相关,除一些必须、共用的中断(如系统时钟中断、片内外设UART中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个容易扩充的中断处理体系。
init_IRQ()
函数被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断入口函数asm_do_IRQ
就可以调用这些函数做进一步处理。
-
总结
ARM架构Linux内核的异常处理体系结构: