linux异常处理体系结构

1、异常的作用

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

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

2、arm9的异常向量表

异常类型处理器模式异常向量高地址向量
复位异常 (reset)特权模式0x000000000xFFFF0000
未定义指令异常(undefined interrupt)未定义指令终止模式0x000000040xFFFF0004
软件中断异常(software abort)特权模式0x000000080xFFFF0008
预取中止异常(prefetch)数据访问终止模式0x0000000C0xFFFF000C
数据中止异常(data abort)数据访问终止模式0x000000100xFFFF0010
外部中断请求(IRQ)外部中断模数0x000000180xFFFF0018
快速中断请求(FIQ)快速中断模式0x0000001C0xFFFF001C

3、linux内核对异常的设置

内核在start_kernel()函数(源码在init/main.c中)调用了setup_arch(&command_line)->[^early_trap_init()]函数和init_IRQ()函数设置了异常(说明:在之前的版本是在trap_init()init_IRQ()设置)。

1)early_trap_init()函数分析(\arch\arm\kernel\traps.c):

early_trap_init 函数被用来设置各种异常向量,包括中断向量。ARM架构的CPU的异常向量基地址可以是0x00000000,也可以是0xffff0000,Linux 内核使用0xffff0000,early_trap_init 函数将异常向量复制0xffff0000处; 部分代码如下:

void __init early_trap_init(void)
{
    unsigned long vectors = CONFIG_VECTORS_BASE;    // CONFIG_VECTORS_BASE 这个宏是一个内核配置项.在 .config 里面。  CONFIG_VECTORS_BASE  = 0xffff0000
    extern char __stubs_start[], __stubs_end[];
    extern char __vectors_start[], __vectors_end[];
    ...
    /*vectors  等于 0xffff0000;  __vectors_start  和  __vectors_end 之间的代码就是异常向量*/
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
    /* __stubs_start和__stubs_end 之间的代码从异常向量跳转去执行跟复杂的代码 */
    memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
    ...
}
知识兔

__vectors_start和__vectors_end之间的代码就是异常向量! 异常向量的代码只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行跟复杂的代码;比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。

        .....
        .equ    stubs_offset, __vectors_start + 0x200 - __stubs_start        /*  */
        .....
    .globl  __vectors_start
__vectors_start:
    swi SYS_ERROR0                       /* 复位时,CPU将执行这条指令 */
    b   vector_und + stubs_offset      /* 未定义指令 */
    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:
    
    .....
知识兔

vector_und 为例。当发生异常时,跳转到异常向量,执行 b vector_und + stubs_offset,然后跳转到下面的代码:

        .....
vector_stub und, UND_MODE         /* "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
        .....
知识兔

vector_stub 宏的功能, 计算处理完异常后的返回地址,保存一些寄存器,然后进入管理模式,最后根据异常的工作模式跳转到 上面 代码 的某个分支 。 这个宏的代码如下:

/*
 * Vector stubs.
 *
 * This code is copied to 0xffff0200 so we can use branches in the
 * vectors, rather than ldr's.  Note that this code must not
 * exceed 0x300 bytes.
 *
 * Common stub entry macro:
 *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
 *
 * SP points to a minimal amount of processor-private memory, the address
 * of which is copied into r0 for the mode specific abort handler.
 */
    .macro  vector_stub, name, mode, correction=0       /* “.macro” 这个伪汇编 是定义一个宏 ,使用方法可以参考这个 https://www.cnblogs.com/Widesky/p/9006954.html    */
    .align  5      /* 4字节对齐 linux下交叉 编译器的对齐方式 2^5bit对齐  */
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     /* 将r0,lr 压入到各自异常的堆栈中 */
    mrs lr, spsr                                              /* 将spsr赋给lr */
    str lr, [sp, #8]        @ save spsr    /* 将lr入栈,即spsr入栈 */

    @
    @ Prepare for SVC32 mode.  IRQs remain disabled.
    @
    mrs r0, cpsr                         
    eor r0, r0, #(\mode ^ SVC_MODE)
    msr spsr_cxsf, r0                           /* 将r0的值赋给spsr_cxsf,此时的状态还是处于und模式 */

    @
    @ the branch table must immediately follow this code
    @
    and lr, lr, #0x0f                            /* lr=lr&0x0f,lr起始就是spsr的值,它保存了进入IRQ模式前的CPU模式,其实是5位控制的,这里只用到4位,用来跳转到不同的处理函数 */
    mov r0, sp                                   /* 将管理模式的sp的值给r0 */
    ldr lr, [pc, lr, lsl #2]                    /* lr = *(pc+lr<<2)。如果在进入IRQ之前是用户模式即是从应用层进入的,那么lr = pc = __und_usr.否则是管理模式也就是处于内核层时发生了IRQ异常 lr = pc+12=__und_svc */
    movs    pc, lr          @ branch to handler in SVC mode          /* 将lr的值给pc,同时将spsr的值赋给cpsr,此时才是进入了管理模式 */
    .endm
知识兔

__und_usr__und_svc 两个不同的分支,只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体,相同,都是调用相应的C函数。未定义指令异常最会调用do_undefinstr函数来处理,跳转的代码如下。

用户模式下发生未定义指令异常

/* __und_usr  用户模式下发生未定义指令异常的处理代码 */
__und_usr:
    usr_entry

    tst r3, #PSR_T_BIT          @ Thumb mode?
    bne __und_usr_unknown       @ ignore FP    //跳转到__und_usr_unknown
    sub r4, r2, #4
        ........     //下面还有一些汇编指令
知识兔
__und_usr_unknown:    mov r0, sp               /* 将栈顶地址,作为参数传入 */    adr lr, ret_from_exception    /* 将返回地址写入到 r0, 处理完C函数后将返回到这里 */    b   do_undefinstr    /*C函数入口,处理未定义指令异常*/

管理模式下发生未定义指令异常

__und_svc:    svc_entry    @    @ call emulation code, which returns using r9 if it has emulated    @ the instruction, or the more conventional lr if we are to treat    @ this as a real undefined instruction    @    @  r0 - instruction    @    ldr r0, [r2, #-4]    adr r9, 1f    bl  call_fpe    mov r0, sp              @ struct pt_regs *regs    bl  do_undefinstr             /*C函数入口,处理未定义指令异常*/    @    @ IRQs off again before pulling preserved data off the stack    @1:  disable_irq    @    @ restore SPSR and restart the instruction    @    ldr lr, [sp, #S_PSR]        @ Get SVC cpsr    msr spsr_cxsf, lr    ldmia   sp, {r0 - pc}^          @ Restore SVC registers     /* 将之前保存到堆栈的寄存器,出栈,并且恢复相应的cpsr寄存器,然后返回断点处继续执行 */

4、小结

未定义指令异常简单的分析了一下linux 下的异常处理体系。
linux 下地异常处理 和 裸机地差不多, 只不过在裸机上地异常向量一般设置为低地址,在linux 下因为使用mmu 所以要将异常向量表的搬移到高端地址。
异常向量和处理异常的代码,搬移前和搬以后的地址如下图:

中断也是异常的一种,因为中断的处理一些中断的处理函数,必须由驱动开发者提供,下一章会单独分析中断管理的框架。

计算机