今天给大家分享的这篇依旧是 2016 年之前痞子衡写的技术文档,花了点时间重新编排了一下格式。前面痞子衡讲过 《嵌入式里的堆栈原理》,本篇算是堆栈原理的工程实践,更具体点说是在 ARM Cortex-M 上的应用。ARM Cortex-M 家族发展至今已经很多代,我们且以最简单的 Cortex-M0 为例来讲述堆栈机制:
1. 基本规则
1.1 R13 / sp 寄存器
R0-R12 为通用寄存器,R13 为系统堆栈指针 sp,堆栈指针是用于访问堆栈,也即系统的 RAM 区。Cortex-M0 中采用了两个堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP),R13 在任何时刻只能是其中一个,默认情况为 MSP,可以通过控制寄存器(CONTORL)来改变。
MSP 是系统复位后(即其处于 Handler Mode)的指定 sp(vector table 的前 4Byte 自动载入),用于处理异常中断。当结束 Reset_Handler 后,cpu 进入正常运行状态(即其处于 Thread Mode),仅在此状态下 PSP 才能被使用,当然 MSP 也可以使用。其后如有硬中断来临,则进入 Handler Mode,如果硬件中断结束,则返回 Thread Mode。
关于 MSP 和 PSP 的选用,其是通过 CONTORL 寄存器来配置,仅在 Thread Mode 下才可设置 CONTORL 寄存器。一般情况下,没有必要使用 PSP,除非是有 os 存在时,MSP 用于 os 内核的 sp,而 PSP 用于 thread 级 app 的 sp,这两个 sp 需严格分开。
在编译器中,可以通过 r13(R13)或 sp(SP)来访问堆栈(具体是 MSP 和 PSP 由当时环境决定);也可以通过指定的 MRS、MSR 指令来访问 MSP 和 PSP。
1.2 栈结构
无 OS 的堆栈结构:
有 OS 的堆栈结构:
1.3 栈操作
Cortex-M0 中堆栈方向是向低地址方向增长,为满堆栈机制。堆栈操作是通过 PUSH 和 POP 来完成操作的。
栈一般放在 ARM 的 RAM 高位区,如某 MCU 中 RAM 地址为 0x20000000-0x20007fff,共 32KByte。栈大小设为 4KByte 的话,其地址一般就放在 0x20007000-0x20007fff,其中 0x20007000 为绝对栈顶,0x20007ffc 为绝对栈底,sp 总是指向相对栈顶。第一个 PUSH 数据被存在绝对栈底(此时绝对栈底也是相对栈顶)。实际上,除了 POP 指令可以从栈顶中取数据外;MOV 指令也可从任意位置取数据,但不会影响栈结构(即不影响其 sp)。
由于 ARM 寄存器均是 32bit,故 PUSH 和 POP 指令均是 32bit 访问,故 sp 指针总是至少 4Byte 对齐(低 2bit 永远为 0)。有时编译器也会分配 8Byte 对齐的栈,这是由于 double 浮点类型需要占用 8Byte,为了处理方便,故将栈设为 8Byte 对齐。
2. 入栈顺序
入栈顺序因编译器、处理器系统、OS 而异,C 语言中并没有强制规定入栈顺序,此处主要是讲 ARM Cortex-M 系列处理器在指定编译器情况下的入栈顺序。
2.1 一般函数调用(通用)
上图展示了在一般函数(无参无局部变量无返回值)嵌套调用时,关于 sp 的操作。在执行 BL FunctionA 指令时,LR 记录的是 BL FunctionA 的下一条顺序指令,在进入 FunctionA 后执行的第一条操作便是 PUSH {LR}即将下一条顺序指令压入栈中,然后才开始执行 FunctionA 函数体。函数体执行结束之后,使用 POP {PC}指令将栈顶数据弹到 PC 中,即可返回继续执行 BL FunctionA 的下一条顺序指令。
2.2 极端函数调用(平台而异)
考虑一种极端情况来详细讲述入栈顺序,即函数含有 4 个参数以上,函数体内定义了多个局部变量,并且还有返回值。这个情况比较特殊,痞子衡专门在 IAR 上做过一次实验,详见今天次条推文(是个长图,看懂需要有一定汇编基础):