计组与微机控制
3.7 子程序与栈
3.7 子程序与栈
上一部分讲的是“如何找到数据”。这一部分继续往下走,变成“如何组织程序执行过程”。
关键链条:
BL 调用子程序 -> LR 保存返回地址 -> 子程序执行 -> 返回到调用点当子程序嵌套、递归、需要局部变量或保存寄存器时,就必须用栈。
栈的本质:
内存中的一段区域 + 栈指针 SP + 入栈/出栈约定
ARM 用 `BL` 指令支持子程序调用。
BL target含义:
LR <- 返回地址
PC <- target
返回时常用:
MOV pc, lr或:BX lr
在 Cortex-M/Thumb 环境中更常见 `BX LR`。
BL 与 LR:本页重点是链接寄存器 LR。
LR 的作用:
> 保存子程序执行完后应返回的地址。
如果子程序不再调用其他子程序,它可以直接用 LR 返回。
如果子程序还要调用别的子程序,LR 会被新的 `BL` 覆盖,所以必须先把旧 LR 保存到栈中。
这就是嵌套调用为什么需要栈的核心原因。
子程序返回的硬件含义:
子程序返回本质是修改 PC。
BX LR等价于:
PC <- LR
区别在于 `BX` 还可以根据地址最低位切换 ARM/Thumb 状态。
STM32 Cortex-M 只支持 Thumb 状态,所以函数返回仍用 `BX LR`,但不会回到 ARM 状态。
大概就是个什么情况呢,当前指令加8是PC值,然后PC-4是下一条指令的位置,提前存到LR(寄存器14)里面,然后跳转子程序执行完PC是r15寄存器对应的,执行完之后把LR再传给PC,之后默认跳到下一个指令的地址,PC再加4,所以下一个指令的位置+8的还是PC值
子程序调用例程:
本页通常通过一个实际例程展示:
BL ProcProc
BX LR理解时看三点:
- `BL` 前 PC 指向调用处;
- 执行 `BL` 后 PC 跳到 `Proc`;
- LR 保存返回点;
- `BX LR` 后 PC 回到调用点之后。
ARM 支持条件执行,因此子程序调用也可以根据条件发生。
典型思想:
CMP r0, #0BLEQ Sub
含义:
如果比较结果为相等,则调用 Sub
否则不调用
这种写法可以减少短分支,但现代 Thumb-2 和流水线处理器中,条件执行的使用策略会受到性能和编码限制影响。
子程序与栈
本页进入栈。
为什么有了 LR 还要栈?
因为:
- 子程序可能嵌套调用;
- LR 可能被覆盖;
- 参数可能太多,寄存器放不下;
- 子程序需要局部变量;
- 需要保存调用者寄存器;
- 递归调用每一层都需要独立现场。
栈就是为这些“临时但必须保存”的信息服务。
栈的基本概念:
栈是一种后进先出结构。
Last In First Out, LIFO
常见操作:
- Push:入栈;
- Pop:出栈。
栈指针 SP 指向当前栈顶或下一个可用位置,具体取决于体系结构和约定。
ARM 中 r13 通常作为 SP。
栈与子程序现场:
本页通常说明栈保存子程序调用现场。
现场包括:
- 返回地址;
- 被调用者需要保护的寄存器;
- 参数;
- 局部变量;
- 上一层栈帧指针。
与中断联系:
> 中断本质也是一种特殊调用,也要保存断点和现场;Cortex-M 会自动压入一部分寄存器。
用普通指令实现调用
SUB r13, r13, #4STR r15, [r13]B Target含义:
1. 栈指针上移,留出 4 字节;
2. 把返回地址压栈;
3. 跳转到目标子程序。
这里体现 RISC 思想:
> 复杂控制动作可以由简单指令组合完成。
从栈中返回:
和之前说的一样返回操作
:LDR r12, [r13], #4
SUB r15, r12, #4或抽象为:
PC <- Memory[SP]
SP <- SP + 4
关键是:
- 返回地址保存在栈中;
- 出栈后恢复 PC;
- SP 必须恢复到调用前状态;
- 有时需要修正 PC,因为取指流水线导致保存的 PC 和期望返回点有偏差。
子程序调用和返回流程图
本页通常用图示栈如何保存返回地址。
理解顺序:
调用前:SP 指向旧栈顶
调用时:SP 改变,返回地址入栈
子程序执行
返回时:从 SP 取回返回地址
SP 恢复
PC 回到调用者
### PPT 348:ARM 与 CISC 返回机制对比
本页继续对比 ARM 和 CISC。
CISC 可能提供一条指令完成:
压返回地址 + 跳转
ARM 更倾向:
用 BL 保存 LR
必要时用 STR/STM 保存 LR 到栈
用 BX/LDM 恢复返回
这种差异是 ISA 设计哲学差异,不是“谁一定更高级”。
返回地址修正
我们继续强调 ARM PC 的特殊性。
在 ARM 状态下,读 PC 得到的值可能是当前指令地址加 8;在 Thumb 状态下常见是加 4。
因此教材示例有时会出现:SUB r15, r12, #4
用于修正返回地址。
学习时要分清:
- 架构规定;
- 流水线可见效果;
- 具体 ARM/Thumb 模式差异。
子程序嵌套:
嵌套调用场景:
main 调用 A
A 又调用 B
B 返回 AA 返回 main
但是A 的 LR 会被 A 调用 B 时的 BL 覆盖
所以 A 在调用 B 前必须保存自己的 LR,通常压栈。
嵌套调用中的 LR 保存
PUSH {LR}BL OtherPOP {LR}BX LR或:
STMFD sp!, {lr}LDMFD sp!, {pc}如果不保存 LR,子程序返回会跳错位置,甚至形成死循环。
