问题
当有操作系统时,我们可以通过代码手动来存储R4-R11,从而保存一个完成的任务快照,那么没有操作系统裸机运行时,如果有中断触发,除了硬件自动保存的中断,R4-R11保存吗?如果不保存,那么中断服务函数如果改动了R4-R11中任意一个寄存器,运行现场不就破坏了吗?
答案
1. 谁污染,谁治理(Callee-saved)
根据AAPCS(ARM 体系结构过程调用标准),在 ARM 的设计规范中,寄存器被分为两类:
Caller-saved (调用者保存):R0-R3, R12, LR, PC, xPSR。
调用者保存的意思是:如果A()调用B(x,y),那么调用者A会将参数x,y存入r0和r1,如果被调用者B函数有返回值,那么也会将返回值放在r0,那么调用者A就需要在call被调用者B之前把r0和r1压栈保存起来。Callee-saved (被调用者保存):R4-R11。
标准规定:如果一个函数(包括 ISR)想要使用这些寄存器,它必须先在自己的入口处把它们压栈,并在退出前弹出恢复。
用上面的理论来解释中断
中断发生时,我们可以将中断的硬件系统认为是调用者,中断处理函数是被调用者,所以进入中断时,硬件自动把它们压栈。中断服务函数(ISR)可以随意使用它们,反正退出时硬件会弹出来恢复,但是中断服务函数在使用R4-R11时,一定要记得保存它们,否则这些寄存器中的值就丢失了。
2.编译器
编译器在生成汇编代码时,会自动检测这个函数是否触碰了 $R4-R11$。如果触碰了,它生成的汇编会像这样:
SysTick_Handler:
PUSH {R4, LR} ; <-- 编译器自动插入!保存它要用的 R4
LDR R4, =0x12345678
; ... 执行逻辑 ...
POP {R4, PC} ; <-- 编译器自动插入!恢复 R4,并返回
所以,在裸机环境下,虽然硬件没帮你在中断入口存 $R4-R11$,但中断函数本身会非常小心地“擦干净自己弄脏的地方”。
3. 既然编译器会存,为什么 OS 还要手动存?
这是最关键的区别。
裸机情况:中断函数执行完后,原路返回。
中断函数把 $R4$ 压入 MSP,执行完后再从 MSP 弹出来。对于被中断的
main函数来说,它根本不知道 $R4$ 曾经被“借走”过。操作系统情况:中断函数执行完后,换路返回。
如果你在中断里决定切换到 TaskB,而中断是从 TaskA 进来的。
如果不手动保存 $R4-R11$,虽然
SysTick_Handler恢复了它自己用过的寄存器,但它无法保存 TaskA此时此刻正在使用的 $R4-R11$ 状态。TaskA 的这些寄存器是它的“私有财产”,如果现在不把它们封存在 TaskA 的栈里,等一会儿切回 TaskA 时,它的 $R4-R11$ 就变成了一堆无法预测的乱码。