← 返回 计组与微机控制

计组与微机控制

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 Proc

Proc

BX LR

理解时看三点:

- `BL` 前 PC 指向调用处;

- 执行 `BL` 后 PC 跳到 `Proc`;

- LR 保存返回点;

- `BX LR` 后 PC 回到调用点之后。

ARM 支持条件执行,因此子程序调用也可以根据条件发生。

典型思想:

CMP r0, #0

BLEQ Sub

含义:

如果比较结果为相等,则调用 Sub

否则不调用

这种写法可以减少短分支,但现代 Thumb-2 和流水线处理器中,条件执行的使用策略会受到性能和编码限制影响。

子程序与栈

本页进入栈。

为什么有了 LR 还要栈?

因为:

- 子程序可能嵌套调用;

- LR 可能被覆盖;

- 参数可能太多,寄存器放不下;

- 子程序需要局部变量;

- 需要保存调用者寄存器;

- 递归调用每一层都需要独立现场。

栈就是为这些“临时但必须保存”的信息服务。

栈的基本概念:

栈是一种后进先出结构。

Last In First Out, LIFO

常见操作:

- Push:入栈;

- Pop:出栈。

栈指针 SP 指向当前栈顶或下一个可用位置,具体取决于体系结构和约定。

ARM 中 r13 通常作为 SP。

栈与子程序现场:

本页通常说明栈保存子程序调用现场。

现场包括:

- 返回地址;

- 被调用者需要保护的寄存器;

- 参数;

- 局部变量;

- 上一层栈帧指针。

与中断联系:

> 中断本质也是一种特殊调用,也要保存断点和现场;Cortex-M 会自动压入一部分寄存器。

用普通指令实现调用

SUB r13, r13, #4
STR 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 返回 A

A 返回 main

但是A 的 LR 会被 A 调用 B 时的 BL 覆盖

所以 A 在调用 B 前必须保存自己的 LR,通常压栈。

嵌套调用中的 LR 保存

PUSH {LR}
BL Other
POP {LR}
BX LR

或:

STMFD sp!, {lr}
LDMFD sp!, {pc}

如果不保存 LR,子程序返回会跳错位置,甚至形成死循环。