Linux 异常处理的层次结构

5.1.1 Linux 异常处理的层次结构

  1. 异常的作用

    异常,就是可以打断CPU正常运行流程的一些事情,如外部中断、未定义指令中断、试图修改只读的数据、执行swi指令(Software Interrupt Instruction)等。当异常发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。

    • 未定义指令异常,操作系统可以利用其来使用一些自定义的机器指令,它们在异常处理函数中实现。
    • 数据访问中止异常,可以将一块数据设为只读,然后提供给多个进程共用,这压根可以节省内存。当某个进程试图修改其中的数据时,将出发“数据访问中止异常”,在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
    • 当用户程序试图读写的数据或执行的指令不在内存中时,也会出发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存, 还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
    • 当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址对齐的问题。
    • 用户程序可以通过 "swi" 指令触发 "swi异常",操作系统在 swi 异常处理函数中实现各种系统调用。
  2. Linux 内核对异常的设置

    内核在 start_kernel() 函数中调用 trap_initinit_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的异常向量基址可以是 0x000000000xFFFF0000 ,Linux内核使用 0xFFFF0000trap_init() 函数就是将异常向量复制到基质处,代码如下:

Linux 异常处理的层次结构_第1张图片

721行中,vectors = CONFIG_VECTORS_BASECONFIG_VECTORS_BASE 是一个配置项(内核配置选项),在Linux顶层目录下,文件.config,搜索CONFIG_VECTORS_BASE

Linux 异常处理的层次结构_第2张图片

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的确定:

Linux 异常处理的层次结构_第3张图片
异常向量表和异常处理程序搬移前后对比

当汇编器看到 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_undvector_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是一个宏,它根据后面的参数undUND_MODE定义了以vector_und为标号的一段代码。这个宏为:

 .macro  vector_stub, name, mode, correction=0
 .align  5

vector_\name:
 .if \correction
 sub lr, lr, #\correction
 .endif

 @
 @ Save r0, lr_ (parent PC) and spsr_
 @ (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_ (parent PC) and spsr_
 @ (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_DataAbortdo_PrefetchAbort

  • arch/arm/kernel/irq.c
    中断处理函数的在这个文件夹中定义,总入口函数asm_do_IRQ,它调用其它文件注册的中断处理函数

  • arch/arm/kernel/calls.S
    swi 异常的处理函数指针被组织成一个表格:swi 指令机器码的位[23:0]被用来作为索引。通过不同的 swi index 指令调用不同的 swi 异常处理函数,被称为系统调用,如 sys_opensys_readsys_write等。

  • 没有使用的异常
    在Linux 2.6.22.6 中没有使用FIQ异常

trap_init()函数搭建了各类异常的处理框架。当发生异常时,各种C处理函数会被调用。这个C函数还要进一步细分异常发生的情况, 分别调用更具体的处理函数。

(2) init_IRQ()函数分析
中断也是一种异常,单独提出来是因为中断的处理于具体开发板密切相关,除一些必须、共用的中断(如系统时钟中断、片内外设UART中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个容易扩充的中断处理体系。
init_IRQ()函数被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断入口函数asm_do_IRQ就可以调用这些函数做进一步处理。

  1. 总结

    ARM架构Linux内核的异常处理体系结构:

    ARM架构Linux内核的异常处理体系结构

你可能感兴趣的:(Linux 异常处理的层次结构)