字符设备驱动-------Linux异常处理体系结构
裸机中断流程
- 外部触发
- CPU 发生中断, 强制的跳到异常向量处
- 跳转到具体函数
- 保存被中断处的现场(各种寄存器的值)
- 执行中断处理函数,处理具体任务
- 恢复被中断的现场
Linux处理异常流程
异常发生时,会去异常向量表找到入口地址,(这算异常发生之后跳转到第一个处理分支),进入异常模式,保护部分现场,强制进入SVC管理模式,根据异常发生前的工作模式,找到异常处理的第二级分支,在该模式下面接过异常模式堆栈中的信息,接着保存异常发生时异常模式还未保存的信息,准备好处理完毕返回处理程序的地址,调用异常处理函数,恢复现场。
处理异常流程中的汇编处理流程:
Linux内核对异常的初始化设置
在内核启动时,内核会在start_kernel函数中调用trap_init,init_IRQ两个函数来设置异常的处理函数
1. trap_init:(arch/arm/kernel/traps.c中定义)
1 void __init trap_init(void) 2 { 3 unsigned long vectors = CONFIG_VECTORS_BASE; /*CONFIG_VECTORS_BASE = 0xffff0000*/ 4 extern char __stubs_start[], __stubs_end[]; 5 extern char __vectors_start[], __vectors_end[]; 6 extern char __kuser_helper_start[], __kuser_helper_end[]; 7 int kuser_sz = __kuser_helper_end - __kuser_helper_start; 8 9 /* 10 * Copy the vectors, stubs and kuser helpers (in entry-armv.S) 11 * into the vector page, mapped at 0xffff0000, and ensure these 12 * are visible to the instruction stream. 13 */ 14 memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); //void *memcpy(void *dest, const void *src, size_t count) 15 memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start); 16 memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz); 17 18 /* 19 * Copy signal return handlers into the vector page, and 20 * set sigreturn to be a pointer to these. 21 */ 22 memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes, 23 sizeof(sigreturn_codes)); 24 25 flush_icache_range(vectors, vectors + PAGE_SIZE); 26 modify_domain(DOMAIN_USER, DOMAIN_CLIENT); 27 }
作用:用来设置各种异常的处理向量,即将异常向量表复制到0xffff0000处。Vectors = 0xffff0000, 地址_vectors_start ~~ __vectors_end之间的代码就是异常向量。
所谓的异常向量就是被安放在固定位置的代码,只是一些跳转指令。发生异常,CPU自动执行这些指令,跳转执行更复杂的代码,比如:保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址 __stubs_start, ~ __stubs_end 之间,第15行,将他们复制到vectors + 0x200 处。
到这里Linux内核异常向量设置的工作就算是完成了。可是想想:设置完这些异常向量之后,异常发生了,CPU是怎么一个处理过程???接着往下分析
从异常向量表来开始入手,__vectors_start和__vectors_end在arch/arm/kernel/entry-armv.S文件中有定义。他们就是内核异常向量表的起始和结束地址。
1 .equ stubs_offset, __vectors_start + 0x200 - __stubs_start 2 3 .globl __vectors_start 4 __vectors_start: 5 swi SYS_ERROR0 //复位时,CPU将执行这条指令 6 b vector_und + stubs_offset //未定义异常时,CPU执行这条指令 7 ldr pc, .LCvswi + stubs_offset //swi异常 8 b vector_pabt + stubs_offset //指令预取中止 9 b vector_dabt + stubs_offset //数据访问中止 10 b vector_addrexcptn + stubs_offset 11 b vector_irq + stubs_offset //IRQ异常 12 b vector_fiq + stubs_offset //FIQ异常 13 14 .globl __vectors_end 15 __vectors_end:
以第一个调转指令“b vector_und + stubs_offset”的分析为例,vector_und是个汇编宏定义,由vector_stub 及后面的参数定义,和c语言里面的宏定义特点是一样的,编译时宏在调用处的展开,就是用宏定义的实体部分去完全取代宏名称,可以直接在该处执行,以下是汇编宏定义规则:
.macro MACRO_NAME PARA1 PARA2 ……
……实体—内容……
.endm
在文件中,找到了 vector_stub 这个宏(以下第二部分代码),它根据后面的参数“ und, UND_MODE”定义了以“vector_und”为标号的一段代码,如下
1 /* 2 * Undef instr entry dispatcher 3 * Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC 4 */ 5 vector_stub und, UND_MODE 6 7 .long __und_usr @ 0 (USR_26 / USR_32) 8 .long __und_invalid @ 1 (FIQ_26 / FIQ_32) 9 .long __und_invalid @ 2 (IRQ_26 / IRQ_32) 10 .long __und_svc @ 3 (SVC_26 / SVC_32) 11 .long __und_invalid @ 4 12 .long __und_invalid @ 5 13 .long __und_invalid @ 6 14 .long __und_invalid @ 7 15 .long __und_invalid @ 8 16 .long __und_invalid @ 9 17 .long __und_invalid @ a 18 .long __und_invalid @ b 19 .long __und_invalid @ c 20 .long __und_invalid @ d 21 .long __und_invalid @ e 22 .long __und_invalid @ f 23 24 .align 5
vector_ 宏定义
1 .macro vector_stub, name, mode, correction=0
2 .align 5 @将异常入口强制进行2^5字节对齐,即一个cache line大小对齐,出于性能考虑
3
4 vector_\name: //und, UND_MODE ......
5 .if \correction @correction=0 所以分支无效
6 sub lr, lr, #\correction
7 .endif
8
9 @
10 @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
11 @ (parent CPSR)
12 @
13 stmia sp, {r0, lr} @ save r0, lr
14 mrs lr, spsr
15 str lr, [sp, #8] @ save spsr
16
17 @
18 @ Prepare for SVC32 mode. IRQs remain disabled.
19 @
20 mrs r0, cpsr
21 eor r0, r0, #(\mode ^ SVC_MODE)
22 msr spsr_cxsf, r0
23
24 @
25 @ the branch table must immediately follow this code
26 @
27 and lr, lr, #0x0f
28 mov r0, sp
29 ldr lr, [pc, lr, lsl #2]
30 movs pc, lr @ branch to handler in SVC mode
31 .endm
以宏“vector_stub und, UND_MODE”为例代入,将其展开为:
1 vector_und: 2 @ 3 @ 此时已进入UND_MOD,lr=上一个模式被打断时的PC值,下面三条指令是保护上个模式的现场 4 @ 5 stmia sp, {r0, lr} @ save r0, lr 6 mrs lr, spsr @ 准备保存上个模式的cpsr值,因为他被放到了UND_MODE的spsr中 7 str lr, [sp, #8] @ save spsr to stack 8 @ 9 @ Prepare for SVC32 mode. IRQs remain disabled. 注意前面的“Prepare”,这里还不是真正切换到SVC,只是准备 10 @ 11 mrs r0, cpsr @ r0=0x1b (UND_MODE) 12 eor r0, r0, #(\mode ^ SVC_MODE) @ 逻辑异或指令 13 msr spsr_cxsf, r0 @ cxsf是spsr寄存器的控制域(C)、扩展域(X)、状态域(S)、标志域(F),注意这里的spsr是UND管理模式的 14 @ 15 @ the branch table must immediately follow this code 下一级跳转表必须要紧跟在这一段代码之后(这一点很重要) 16 @ 17 and lr, lr, #0x0f @ 执行这条指令之前:lr = 上个模式的cpsr值,现在取出其低四位--模式控制位的[4:0],关键点又来了:查看2440芯片手册可以知道,这低4位二进制值为十进制数值的 0-->User_Mode; 1-->Fiq_Mode;
//2-->Irq_Mode; 3-->SVC_Mode; 7-->Abort_Mode; 11-->UND_Mode,明白了这些下面的处理就会恍然大悟,原来找到那些异常处理分支是依赖这4位的值来实现的 18 mov r0, sp @ 将SP值保存到R0是为了之后切换到SVC模式时将这个模式下堆栈中的信息转而保存到SVC模式下的堆栈中 19 ldr lr, [pc, lr, lsl #2] @ LDR的稀有用法:将pc+lr*4的计算结果重新保存到lr中,我们知道pc是指向当前指令的下两条指令处的地址的,也就是指向了“.long __und_usr” 20 movs pc, lr @ branch to handler in SVC mode 前方高能!关键的地方来了!在跳转到第二级分支的同时CPU的工作模式从UND_MODE强制切换到SVC_MODE,这是由于MOVS指令在赋值的同时会将spsr的值赋给cpsr 21 ENDPROC(vector_und) 22 .long __und_usr @ 0 (USR_26 / USR_32)运行用户模式下触发未定义指令异常 23 .long __und_invalid @ 1 (FIQ_26 / FIQ_32) 24 .long __und_invalid @ 2 (IRQ_26 / IRQ_32) 25 .long __und_svc @ 3 (SVC_26 / SVC_32)运行用户模式下触发未定义指令异常 26 .long __und_invalid @ 4 其他模式下面不能发生未定义指令异常,否则都使用__und_invalid分支处理这种异常 27 .long __und_invalid @ 5 28 .long __und_invalid @ 6 29 .long __und_invalid @ 7 30 .long __und_invalid @ 8 31 .long __und_invalid @ 9 32 .long __und_invalid @ a 33 .long __und_invalid @ b 34 .long __und_invalid @ c 35 .long __und_invalid @ d 36 .long __und_invalid @ e 37 .long __und_invalid @ f
代码注释出自:https://blog.csdn.net/clb1609158506/article/details/44348767
小结:vector_stub的宏功能:计算处理完异常后的返回地址,保存一些寄存器(r0,lr, spsr),然后进入管理模式,最后根据被中断的工作模式调用第22行-37行中的某个跳转分支。发生异常时,CPU根据异常类型进入某个工作模式,但是很快 vector_stub又会强制CPU进入管理模式,在管理模式下进行后续处理。
eg: 执行到“movs pc, lr”这一句,找到了branch table中的一项,现在我们继续往下分析,假设进入UND_MODE之前是User模式,那么接下来会到__und_usr分支去继续执行
__und_usr标号也是在该文件中定义,
分支跳转 __und_usr ——> b __und_usr_unknown —–> b do_undefinstr —-> 最终调用C函数进行复杂的处理 在arch/arm/kernel/traps.c中
2.Init_IRQ函数分析
中断也是一种异常,之所以单独列出来,是因为中断的处理与具体开发板密切相关,除了一些必须、共用的中断(比如系统时钟中断,片内外设UART中断)之外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个极易扩充的中断处理体系。
Init_IRQ函数(arch/arm/kernel/irq.c 中定义)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数 asm_do_IRQ 就可以调用这些函数作进一步处理。
Linux 异常处理体系结构: