linux异常处理体系结构
1、异常的作用
异常,就是可以打断CPU正在运行流程的一些事情,比如外部中断、未定义指令、试图修改只读数据、执行swi指令(中断指令)等。当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。
- 未定义指令异常: CPU在执行一些未定义的机器指令时,触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义指令。
- 数据访问终止异常: 将一块数据设为只读的,提供给多个进程共用, 这样可以节省内存。当某个进程试图修改其中的数据时,将触发"数据访问终止异常", 在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
- 当用户程序试图读写的数据或执行的指令不在内存中时,也会出发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存, 还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
- 当程序使用不对齐的地址访问时,也会触发"数据访问终止异常", 在异常处理程序中先使用多个对齐的地址读出数据。对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址的对齐的问题。
- 用户程序可以 "swi" 指令触发 "swi异常" ,操作系统在swi异常处理函数中实现各种系统调用。
2、arm9的异常向量表
异常类型 | 处理器模式 | 异常向量 | 高地址向量 |
---|---|---|---|
复位异常 (reset) | 特权模式 | 0x00000000 | 0xFFFF0000 |
未定义指令异常(undefined interrupt) | 未定义指令终止模式 | 0x00000004 | 0xFFFF0004 |
软件中断异常(software abort) | 特权模式 | 0x00000008 | 0xFFFF0008 |
预取中止异常(prefetch) | 数据访问终止模式 | 0x0000000C | 0xFFFF000C |
数据中止异常(data abort) | 数据访问终止模式 | 0x00000010 | 0xFFFF0010 |
外部中断请求(IRQ) | 外部中断模数 | 0x00000018 | 0xFFFF0018 |
快速中断请求(FIQ) | 快速中断模式 | 0x0000001C | 0xFFFF001C |
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下使用了高地址
异常向量和处理异常的代码,搬移前和搬以后的地址如下图:
中断也是异常的一种,因为中断的处理一些中断的处理函数,必须由驱动开发者提供,下一章会单独分析中断管理的框架。