注意以下过程描述了两种armv7指令集的内核的中断表现(cortex-A7和cortex-m3),但是cortex-A7和cortex-m3表现很不一样,因为Cortex-m3只有用户级和特权级两种级别,而Cortex-A7有9种模式,并且有的寄存器各个模式有自己单独的备份,例如Cortex-m3发生中断的时候会硬件自动入栈LR寄存器,而Cortex-A7好像没有这个操作,原因应该是Cortex-m3 用户级和特权级用的都是一个LR寄存器,而Cortex-A7,各个模式基本有自己独享的LR寄存器,也就没必要保存LR寄存器。
函数调用
首先,函数调用是预料范围内的代码执行,是完全可控的,当前执行的函数调用另外一个函数时,是从当前代码段通过跳转指令主动跳转到另外一个代码段,只需保存跳转之前的栈顶指针(fp)到栈空间,保存跳转指令的下一条指令的地址到lr寄存器,无须保存所有寄存器的值(如果C函数有使用R4-R11寄存器,还是要保存一下滴,这个C编译器会帮我们完成,无需担心,下面会详细说明),然后还要使用寄存器r0-r3传递函数参数,或者使用栈空间传递函数参数。所以严格来说,函数调用过程中,不能严格说是现场保护。
函数调用可以参考另一篇文章https://blog.youkuaiyun.com/xiaoyink/article/details/107052060
(注:但是一个例外是 如果C语言调用汇编程序时,如果汇编中使用了R4-R11寄存器,在汇编的开头和结尾要手动入站和出栈R4-R11寄存器,保护现场。如果,不保护R4-R11会有什么影响,假设调用汇编后,C程序在调用汇编前将一个变量读入到了 R4寄存器,C程序在调用汇编后如果要使用变量的值,可能直接从R4寄存器中取,而不是去内存中取,除非 此变量有 volatile修饰,所以汇编程序要保证不破坏R4-R11寄存器,因为C的编译器遵守这种C函数调用标准约定,所以编译器默认以为调用的汇编程序也遵守此约定,另外汇编程序不需保护R0-R3、R12寄存器,因为编译器认为调用一个函数后,无论是汇编函数还是C函数,R0-R3、R12寄存器一定被破坏过了,所以编译器不会像信任R4-R11寄存器那样,在函数调用之后发生之后还去肆无忌惮地使用R0-R3、R12寄存器,再者说,在函数调用过程中,R0-R3担负着传递参数和接收返回值的任务,所以一旦函数调用发生后,编译器不再信任R0-R3、R12寄存器,但是仍然信任R4-R11,寄存器,所以汇编程序一定不能辜负编译器的信任,一定要手动保护好R4-R11寄存器(C函数也会保护R4-R11,只不过C编译器帮我们完成),至于R0-R3、R12寄存器,汇编程序可以随意破坏。)
但是,中断、进程切换这两个过程和函数调用不一样,中断和进程切换实质都是靠中断来完成的(系统调用或者系统滴答时钟中断来完成进程切换,但是普通中断和进程切换又不完全相同),这里就先简单分析中断过程。中断是不可预测的,所以中断产生时,硬件以及中断服务程序中的代码要保存所有的寄存器数据。我们以函数调用和中断比较来说明为什么。假设函数调用前涉及到对一个局部变量的赋值操作,赋值操作编译成汇编语句可能要至少两句(假设是,把23 赋值给,0x10002100处的内存:mov r0, #23 str r0, [0x10002100]),那么编译器在编译的时候,不可能在两句汇编语句之间加上函数调用语句,肯定要等赋值完全完成后再进行函数调用相关语句,但是中断就不同了,中断可能随时发生,可能在两句赋值功能的语句中间出现,甚至可能在单独一句汇编语句执行一半的时候出现(例如缺页异常),那么如果不保护现场,把所有可能在中断服务程序中破话的寄存器全部保存下来,那么中断返回后赋值语句很可能执行失败,所以,中断发生的时候,cpu硬件和中断服务程序将相互配合,将中断时刻所有的寄存器数据(严格来说也不是所有,是中断服务函数可能破坏的寄存器)保存到栈空间(arm-linux是内核栈,cortex-m3单片机可能是内核栈也可能是用户栈)
cortex-M3 中断调用过程
- 入栈
中断发生后,中断服务函数运行前,会先把xPSR, PC, LR, R12以及R3‐R0总共8个寄存器由硬件自动压入适当的堆栈中(中断前使用的MSP就压入MSP,中断前使用的是PSP就压入PSP),为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?详见C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,AAPCS, Ref5),C函数实现的函数(不论是普通函数,还是中断服务例程),一般都只用R0-R3、R12寄存器,并且在普通函数中这些寄存器被用来传递参数,所以C程序中断服务函数会破坏R0-R3、R12,不会破坏R4-R11(即使破坏R4-R11,编译器也会生成R4-R11的压栈和出栈语句,还是相当于没有破坏R4-R11),所以硬件为了配合C函数实现的中断服务函数,会在中断服务函数运行之前,硬件压栈R0-R3、R12寄存器,在中断服务函数结束后,硬件出栈,这样就保护了现场,C函数调用标准约定详见https://blog.youkuaiyun.com/itismine/article/details/4752489
入栈顺序以及入栈后堆栈中的内容 - 取向量
当数据总线(系统总线)正在为入栈操作而忙得团团转时,指令总线(I‐Code总线)可
不是凉快地坐着看热闹——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表
中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线
的好处:入栈与取指这两个工作能同时进行。 - 更新寄存器
在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:
SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后,将由MSP负责对堆栈的访问。
PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
PC:在向量取出完毕后,PC将指向服务例程的入口地址,
LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”,并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4位则有另外的含义 - 异常返回
在CM3中,是通过把EXC_RETURN往PC里写来识别返回动作的。合法的EXC_RETURN值共3个
详见《Cortex-M3权威指南》
Cortex-A7中断调用过程
Cortex-A7只有8个异常中断,实际使用的只有7个,所有的外部中断都是用一个中断,即IRQ中断,不像Cortex-M3的芯片,每一个外部中断在中断向量表中都有自己的一席之地,Cortex-A7的处理方式是几十个上百个外部中断都是用一个中断处理函数,IRQ的中断处理函数,这个函数一般是由汇编编写的,然后再这里再调用不同的C语言函数处理不同的外部中断。
下面我们只讨论IRQ中断。
中断发生后Cortex-A7没有硬件入栈的过程,硬件只需要做两件事:
1.把PC寄存器保存到LR寄存器中,注意这里的LR寄存器是IRQ模式特有的LR寄存器,不是Sys和User模式的LR寄存器;
2.把CPSR寄存器存放到SPSR寄存器,当然SPSR寄存器也是IRQ特有的寄存器
具体描述如下:
An IRQ exception is raised by external hardware. The core performs several steps
automatically. The contents of the PC in the current execution mode are stored in
LR_IRQ. The CPSR register is copied to SPSR_IRQ. The CPSR content is updated so that
the mode bits reflects the IRQ mode, and the I bit is set to mask additional IRQs. The PC
is set to the IRQ entry in the vector table.
摘自ARM Cortex-A(arm V7)编程手册V4.0.pdf
所以中断发生时Cortex-A7不需要硬件保存LR寄存器,只需要硬件保存PC和CPSR寄存器,因为他们各个模式的LR寄存器都是自己独用的,而Cortex-M3硬件入栈这个寄存器,是因为用户级和特权级是公用这个寄存器的,Cortex-A7的各个模式的寄存器如下:
来自 ARM Cortex-A(armV7)编程手册V4.0
来自正点原子开发指南,
两者在irq的SP和LR显示不一致,可能是官方的错了
http://www.openedv.com/forum.php?mod=viewthread&tid=313383&page=1#pid1101725
然后对寄存器R0-R3,R12,以及LR_IRQ和SPSR_IRQ的保存需要IRQ中断处理函数手动入栈保存,完成现场保护,防止之后的汇编指令或者C函数破坏它们。
那可能又要问了,为什么这些寄存器在Cortex-M3中都是硬件主动保存,而Cortex-A7需要汇编函数手动保存,Cortex-A7手动保存的过程中,会不会被其他中断打断,导致这些寄存器还没来得及入栈就被破坏了?
先回答第一个问题
Cortex-M3硬件保存,原因1上面已经提到,LR(注意是User或Sys或其他模式的LR,不是LR_IRQ) 这个寄存器Cortex-A7是各个模式独享的,所以不用硬件保存,而PC寄存器两者都需要硬件保存,Cortex-M3是硬件入栈,而Cortex-A7是把PC硬件保存到LR_IRQ,CPSR寄存器两者也都需要硬件保存,Cortex-M3是硬件入栈保存,Cortex-A7是硬件保存到SPSR_IRQ寄存器。所以Cortex-A7的汇编函数还需要手动保存LR_IRQ和SPSR_IRQ;
那R0-R3, R12寄存器为什么Cortex-M3需要硬件保存,而Cortex-A7则是软件手动保存?
Cortex-M3之所以要硬件入栈R0-R3,R12的盘算是要迎合C语言直接编写中断处理函数,Cortex-M3的所有外部中断多在中断向量表中有自己的处理函数,而这些处理函数往往需要用户自己实现,让用户挨个用汇编实现不太现实,所以硬件入栈R0-R3,R12可以使用户直接用C来实现中断处理函数,而Cortex-A7的所有外部中断都调用同一个中断处理函数,所以只需要写一遍,即使用汇编实现,也只需要繁琐一次,所以不必迎合C语言。其实Cortex-m3硬件入栈R0-R3,R12 还有一些安全因素会在第二个问题中回答。
回答第二个问题
对Cortex-A7而言,在中断处理函数中手动保存R0-R3,R12,以及LR_IRQ和SPSR_IRQ,会不会因为其他中断的打断而出错呢?
如果在手动保存现场之前其他外部中断能够打断,肯定会破坏LR_IRQ,以及LR_SPSR,但是其他外部中断肯定不会打断,因为所有的外部中断都是IRQ中断,所以在IRQ中断中不会被其他外部中断打断,因为不可能自己把自己打断,而另外6种中断(见表17.1.2.1)发生时肯定会保护现场,处理结束并恢复现场,所以这些寄存器并不会被改变,而且LR_IRQ和SPSR_IRQ是IRQ独享的,其他中断发生时运行在其他模式,所以无法访问,R0-R3,R12会根据中断保护现场和恢复现场的机制得到保护。所以Cortex-A7通过让汇编指令手动保存这些寄存器是非常安全的。
而对Cortex-M3而言,必须硬件入栈保存R0-R3,R12,LR,PC,CPSR,因为它的各个外部中断使用的是不同的中断处理函数,会相互打断,而且LR寄存器是共享的,不是独享的。所以如果让Cortex-M3软件保存上述寄存器,可能会由于中断相互打断而导致数据错误,另外由于中断服务函数都是C编写的,C函数肯定会破坏R0-R3,R12这些寄存器,并不会保护这些寄存器,参照上述C函数调用标准约定。
但是这里又有一个新的问题,Cortex-A7,所有的外部中断都是用IRQ这一个中断,所以不能自己打断自己,这明显是不合常理的,不同优先级的外部中断理应是高优先级的外部中断可以打断低优先级的外部中断,那为了实现这样的效果IRQ中断服务函数是怎么编写的呢?其实只需要在软件保存好现场之后,也就是R0-R3,R12,以及LR_IRQ和SPSR_IRQ这些寄存器入栈,切换CPU的运行模式到SVC模式,然后再有IRQ外部中断发生时就可以打断自己了。(注:这只是裸机自己编写中断处理函数的简单处理,具体的跑linux系统是怎么处理的,还未深究)代码如下:(参考正点原子)
IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */
/*至此现场保护完成*/
mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
/* 由于下面要调用C函数,所以先保存r0-r1,其实r3 和r12也会被破坏,但是这个接下来的语句并没有使用,所以不用保存 */
push {r0, r1} /* 保存r0,r1 */
cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
/*下面调用blx指令会破坏lr,所以先保存*/
push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */
pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
/*中断恢复现场时不能被其他外部中断打断,所以进入IRQ模式*/
cps #0x12 /* 进入IRQ模式 */
/*C程序调用之后要重新获取r0-r1的值,它们已经被破坏*/
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */ /*这一步详见手册*/
/*恢复现场*/
pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */
pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */
str r0, [r1, #0X10] //中断执行完成需要写中断号到GICC_EOIR寄存器,表示中断处理完成
subs pc, lr, #4 //由于流水线的存在,PC指向取指的指令,PC-4就是译码的指令,也就是中断完成后需要执行的指令