本文由 AI 辅助生成并经自动审查;尚未完成真实硬件验证。涉及具体芯片、时序和电气行为时,请以文末第一方资料和实际测试为准。
栈:从 SP(栈指针)与内存边界到可信的调用现场 #
L036 · debugging-verification / stack · 约 80 分钟
本课只有一个核心目标、两个新核心概念。Host Fake(主机替身)代码可以在主机运行;STM32F407xx 代码是参考实现,待目标工程编译和上板验证。本次未进行目标编译、烧录、调试器连接或波形/时序测量,hardware_verified=false。课程已通过 Reviewer 的文档与主机模型审查,status=reviewed;这不代表目标工程已验证或用户已掌握,审查记录见本次运行目录的review.md。
1. 本节目标 #
**核心目标(与本次 plan.json 一致):**建立以 SP、栈边界和编译产物交叉验证栈使用量与调用现场的证据方法,并为 STM32F407xx 规划可核对但尚未上板的最小采样与调试路径。
新核心概念仅两个:
- 栈边界与动态使用量的证据边界。
- 调用现场重建的证据边界。
学完后,应能对“SP 还在 RAM”“链接脚本给栈留了空间”“回溯里有三个函数”等说法补齐条件,并给出一项可能推翻当前判断的下一步观察。不会在本课实现通用栈展开器,也不会提前实现 RTOS(实时操作系统)任务栈、MPU(存储器保护单元)防护或 FPU(浮点单元)懒堆叠解析。
阅读路线:先看第 2~6 节及 8.1 的主机最小例,再看 7、8.2 和第 9 节的目标实现,最后做第 12~15 节。完整源码在本课 host/、stm32/;第 9 节展开关键文件,测试用例无需手工抄写。
2. 它解决什么问题 #
2.1 三个容易过度推断的现场 #
- 暂停后 SP 落在 SRAM(静态随机存取存储器)内,所以栈没问题?**不够。**一个地址在 SRAM 内,不等于它属于本构建为这个栈保留的区间,也不说明此前没有越界后又恢复。
- 链接脚本中的栈预留为 N 字节,所以运行只用了 N 字节以内?**不够。**静态预留描述空间安排,运行时 SP 描述一个时刻;链接器不负责持续检查 CPU 的每次压栈。[S1 §2.1.2;S2 §2;S10]
- 栈内有几个 Flash 地址,所以已找到调用链?**不够。**数据、旧帧和返回信息都可能长得像代码地址。需要该构建的指令、调试信息与可信现场互证,而不是在 SRAM 里“捡地址”。普通 C 函数的物理帧也不是异常自动帧;优化可以省略帧指针、内联函数或把末尾调用变为跳转。[S1 §2.3.7;S8]
本课把结论分为三类:可判定、证据不足、输入无效。这不是一个“是否溢出”的万能布尔值。例如 SP 越界可能是栈耗尽,也可能是错栈、错误链接符号或寄存器损坏;拒绝用它计算正常剩余量,不等于已经找到了根因。
2.2 先看一遍能够观察的流程 #
同一构建的栈保留区间 [low, high)
+ 说明采样时刻和所属栈的 SP
-> 先核对身份、范围与序列
-> 才计算本次占用、剩余距离、最低已观测 SP
-> 保存“当前证据能支持什么”
-> 如需调用链,再与 PC(Program Counter,程序计数器)/LR(Link Register,链接寄存器)、异常帧、ELF(Executable and Linkable Format,可执行与可链接格式)/反汇编互证主机最小例只注入三个数字地址,不访问这些地址处的内存。预期看到占用从 0 到 128 再到 64,而“已观测峰值”仍为 128。这先证明算法会保留历史,不证明真实 MCU 的峰值就是 128。
3. 必要前置知识 #
以下主题已在 completed 中出现,但“已授课”不等于“已掌握”。不熟悉时先回看对应课程:
| 前置主题 | 本课只唤回的用途 |
|---|---|
| pointer、storage-duration | 地址值不是可安全解引用的许可;诊断历史必须比单次采样活得更久 |
| compiler、linker-script、elf-map | 区间由实际链接安排决定;地址解释需要这次构建,而非另一个同名 ELF |
| stack-heap、memory-layout | 不把所有 RAM 都当作主栈,也不把堆和静态对象当成可用栈 |
| vector-table、startup | 初始 MSP(Main Stack Pointer,主栈指针)来自启动映射下的向量表首项;随后启动代码与 main 会继续消耗栈 |
| interrupt-priority、fault-analysis | 异常会建立有限现场;异常可嵌套,入栈失败时不能无条件信任帧 |
| clock-tree、register-view、breakpoint-watchpoint | 样本必须附带时钟、复位与暂停条件;读取动作也会扰动现场 |
没有“必须先会 RTOS”的隐藏要求。本课使用裸机、特权 Thread(线程)模式、MSP 的受控采样,不配置 PSP(Process Stack Pointer,进程栈指针)任务切换。
4. 核心原理 #
4.1 核心概念一:栈边界与动态使用量的证据边界 #
4.1.1 先消除“栈顶/栈底”的语言歧义 #
本文不用“栈顶”代指固定的某个端点,而统一使用:
low:为这个栈保留的最低地址。high:保留区的高地址开区间端点,也是本实验的初始空栈 SP。sp:指定时刻的活动栈指针。
Cortex-M4 使用满递减栈:压栈朝较低地址发展。普通编译代码怎样安排局部变量与保存寄存器,仍取决于编译器与构建;不能由 C 源码中的局部变量顺序推断。[S1 §2.1.2;S8]
高地址 high ───────── 初始空 SP,不能把 high 当成可读栈字
已分配/曾保存的区域(具体内容需另证)
sp ───────── 本次样本边界
向下继续分配的地址距离
低地址 low ───────── 保留区下界只有确认 low <= sp <= high、所属栈一致、构建一致、样本有效后,才计算:
本次从初始 high 向下占用的地址跨度 = high - sp
本次距下界的地址距离 = sp - low
已采样点中的最大占用跨度 = high - min(有效样本的 sp)这些是地址跨度,不是逐字节“正在存活的 C 对象大小”,也不是保证下一条指令能安全调用多少层。sp == low 数学上可计算出剩余 0;这不是安全结论。sp == high 对空栈指针可接受,但读取 *(uint32_t *)high 仍越出区间。
例子(只是假地址,不是推荐 STM32 链接布局):
| 时刻 | SP | 本次跨度 | 距下界 | 已观测最大跨度 |
|---|---|---|---|---|
| 1 | 0x20001400 | 0 | 1024 | 0 |
| 2 | 0x20001380 | 128 | 896 | 128 |
| 3 | 0x200013C0 | 64 | 960 | 128 |
最后一次 SP 上升,历史最小 SP 不能跟着上升。本课历史绑定 {low, high, stack_id, build_tag};链接边界、所属栈或固件改变后,必须由调用者明确新建/清零历史,不能暗中把两组实验拼接。
4.1.2 五种量不能互相冒充 #
| 证据 | 直接支持 | 不能直接支持 |
|---|---|---|
| 链接保留区、向量首项 | 本构建计划把栈放在哪里、初始 MSP 取何值 | 运行峰值、已执行路径是否安全 |
| 某次有效 SP | 该栈在该时刻的占用跨度 | 过去/未来最低 SP,完整函数链 |
| 多次最低已观测 SP | 采样覆盖到的最大跨度 | 两次采样间的瞬时峰值、未执行路径 |
| 填充图案扫描 | 扫描时哪些字节与原图案不同 | 没改变图案的区域从未被分配、真实最坏峰值 |
| 静态分析/压力实验与前述证据联合 | 在明确假设下扩大置信范围 | 不说明假设就宣称对所有输入、嵌套与库路径完成证明 |
例如,函数把 SP 减小 64 字节,却只向其中 8 字节写值,填充扫描可能看不到其余 56 字节的分配;若写入值恰好等于填充图案,也会漏计。扫描与 SP 采样测的是不同现象。最坏情况还取决于调用路径、编译优化、库函数、中断嵌套和受支持的浮点异常上下文,不能用一个观测量替代全部。[S1 §2.3.7;S8;本课模型反例]
4.1.3 对象、所有权与调用流 #
| 对象 | 定义在哪里 | 谁持有 | 谁注入 | 调用如何流动 |
|---|---|---|---|---|
StackBounds |
host/stack_evidence.h;目标值来自链接符号 |
Host 测试夹具;目标模块的静态 bounds |
测试或 StackProbe_Init |
边界先到位,再允许观察 |
StackSample |
同一头文件 | 一次调用的局部对象 | Fake 序列或汇编快照的适配代码 | sample -> Observe,不从地址读取内存 |
StackHistory |
同一头文件 | 测试夹具或目标模块,跨采样保留 | 初始化时显式置零 | 所有核验通过后才更新最低 SP |
StackReport |
同一头文件 | 调用者 | Observe 写入 |
先检查 status/has_numbers,再显示数值 |
build_tag 是调用者提供的 32 位索引,不是唯一性证明或完整固件哈希。实验记录必须把它映射到完整 ELF 哈希、构建选项与目标身份;发生冲突时这组历史不应继续合并。
4.2 核心概念二:调用现场重建的证据边界 #
4.2.1 先问“哪个时刻、哪一种 LR” #
SP(Stack Pointer,栈指针)不是调用链。PC(Program Counter,程序计数器)说明执行位置;LR(Link Register,链接寄存器)保存与返回有关的信息。普通 BL(Branch with Link,带链接的分支)调用会写 LR,而异常入口把 **EXC_RETURN(异常返回编码)**写入异常处理函数的 LR。异常帧里另有一个保存的 LR,它属于被打断的上下文。这三者不能混用。[S1 §2.1.3、§2.3.7]
| 来源 | 本课采用的准确称呼 | 使用限制 |
|---|---|---|
| 调试器暂停读出的 PC/LR | 暂停现场的 PC/LR | 与暂停时刻的 SP 配对;工具显示细节按实际工具核对 |
本课汇编保存的 pc_anchor |
采样代码内的定位锚点 | 不是任意指令处暂停所得的完整 PC 快照 |
汇编入口的 return_lr |
返回 StackProbe_Capture 的信息 |
不是异常入口 EXC_RETURN,也不是业务函数完整调用链 |
| 原始异常入口 LR | EXC_RETURN | 必须在 C handler 覆盖/保存 LR 之前取得 |
| 有效基本异常帧中的 LR/PC | 被打断上下文的有限现场 | 不自动揭示更老的软件函数帧;PC 的故障语义回到 L035 分析 |
4.2.2 MSP/PSP 的选择必须带上下文 #
MSP(Main Stack Pointer,主栈指针)和 PSP(Process Stack Pointer,进程栈指针)是两个 SP。复位后的 Thread 模式默认使用 MSP;Thread 模式可由 CONTROL.SPSEL 选择 PSP;Handler(异常处理)模式使用 MSP。IPSR(中断程序状态寄存器)为 0 表示 Thread,否则表示当前异常号。[S1 §2.1.2、§2.1.3]
“此刻活动栈”:
IPSR != 0 -> MSP
IPSR == 0 && CONTROL[1]=0 -> MSP
IPSR == 0 && CONTROL[1]=1 -> PSP
“被异常打断时使用的帧”:
由原始异常入口 EXC_RETURN 选择,而非由此刻 handler 的活动 MSP 猜测。两者并不矛盾:Thread 用 PSP 时,异常自动保存 Thread 现场到 PSP,随后 handler 使用 MSP;更高优先级异常打断 handler 又会涉及 MSP。不能把 PSP 的帧和 MSP 的边界拿来相减,也不能把已经进入多层 C handler 后的 MSP 当作最早异常入口 SP。[S1 §2.3.7]
4.2.3 只解析已经复制、来源明确的基本帧 #
Cortex-M4 的非浮点基本帧为 8 个 32 位字;在原始入口 SP 指向的基本帧内依次是 R0、R1、R2、R3、R12、保存的 LR、保存的 PC、xPSR(组合程序状态寄存器)。帧对齐还可能增加一个字,必须结合 CCR(配置与控制寄存器)的 STKALIGN 位与保存的 xPSR[9] 判断。完整依据见 PM0214 Figure 12、Table 18。[S1 §2.3.7、§4.4.7]
本课 StackEvidence_BasicFrame 的输入是已经合法复制的定长数组,不是一个让函数去盲读的裸 SRAM 地址。它只接受 0xFFFFFFF1/F9/FD 三种基本帧返回编码;对 0xFFFFFFE1/E9/ED 浮点扩展编码返回“证据不足”,本课不解释扩展帧布局或懒保存。其他编码、Thumb 状态不符、长度不够等返回“输入无效”。
还检查:
- entry SP 的区间、构建与栈身份;确认这里的 SP 是入口 SP,不是后来 handler 的 SP。
- CFSR(可配置故障状态寄存器)中的入栈、出栈、浮点懒保存错误;出现这些错误就不输出可信帧。
- 用
high - sp >= 所需字节核验,不先计算可能回绕的sp + 字节;若 xPSR[9] 表示对齐填充,基本帧入口必须满足 8 字节对齐。 - 返回 Handler/Thread 的编码与保存 xPSR 中的异常号是否自洽,并拒绝架构保留号;具体芯片是否实现某个外部 IRQ 仍须另查。
- PC 是否满足本实验选定的同构建代码区间策略。合法的 RAM 执行代码若未纳入区间,会被本模型保守拒绝;这不证明硬件帧本身有错。
has_frame=1 只表示输入满足这些必要条件,不证明数组来自真实异常、不证明每个保存字未损坏,也不构成完整根因/调用链验证。SRAM 内的错误数据也可能凑巧满足所有检查。[S1 §2.3.7、§2.4、CFSR;本课支持范围]
4.2.4 软件帧必须用同一构建解释 #
普通函数保存了哪些寄存器、局部区多大、有没有帧指针,要看实际反汇编、ABI(应用二进制接口)与展开信息,而不是套用异常帧的八字布局。
- 内联:函数代码融入调用者,不一定存在独立物理帧。
- 尾调用/同级调用优化:末尾调用可能变成跳转,不保留普通
BL链式现场。 - 省略帧指针:没有某个固定寄存器串起来的简单链;即使指定
-fno-omit-frame-pointer,也不能据此保证所有函数都有这种帧。 - 寄存器/栈损坏:展开器可能停止,也可能输出貌似合理的错误结果。
- 调试信息不匹配:相同地址用另一个 ELF 解释,可能得到完全不同的函数与行号。[S8]
ELF(Executable and Linkable Format,可执行与可链接格式)保存构建产物;DWARF(调试信息格式)帮助调试器关联源代码与机器状态;unwind(栈展开)是根据寄存器、栈内容及构建信息恢复调用者现场的过程。.debug_* 与 .ARM.exidx 等信息承担的职责并不相同,不能假定 -g 自动保证某一种展开表或目标工具总能完整回溯。**本次未核对实际 GNU Arm 编译器/GDB 的版本及展开算法,目标工具特定结论待核对。**本课只给出待执行的产物与工具检查路径,不制造展开保证。[S8、S9;S11 的访问边界]
该机制的对象关系:原始快照由采样模块持有;帧副本由合法采集方提供;ELF、链接脚本、反汇编和选项由构建方保存;分析者负责注入匹配的构建身份;解释流是 原始证据 -> 有效性检查 -> 地址/指令对应 -> 带条件的判断,不是“扫描出地址 -> 宣布调用链”。
5. 关键术语与直观模型 #
这些都是两个核心概念的辅助词汇,不是新增独立课题:
| 术语 | 本课中的准确含义 |
|---|---|
| 满递减栈(full descending stack) | 栈向低地址增长;不能因此推断所有 C 帧布局 |
| 栈边界 | 本构建明确保留给指定栈的区间,不是 RAM 总边界 |
| 最低已观测 SP | 有效采样集合中的最小值,非所有执行时刻的最小值 |
| 栈高水位(stack high-water mark) | 常用于指代峰值相关指标;本文每次明确是“采样峰值”还是“填充扫描改写范围” |
| EXC_RETURN | 异常入口 LR 中的返回编码,不是普通 Flash 返回地址 |
| map file(链接映射文件) | 链接安排的交叉检查材料;不测运行峰值,下一课再深入 |
| Host Fake | 在主机注入可控数据验证逻辑,不把主机调用帧当作 Cortex-M 的帧 |
NOLOAD |
链接段的属性,不等于 CPU 运行时会保护它或启动代码绝不写它;实际 ld 版本与脚本语义须核对 [S10] |
6. 从输入到结果的完整流程 #
6.1 每条证据都走同一条判断链 #
- 标定身份:记录目标/复位会话、完整固件哈希、构建选项;将模块的
build_tag对应到这份记录。 - 建立边界:从实际链接脚本、ELF 符号、Map、向量首项互证主栈区间,不从芯片总 RAM 猜区间。
- 标定采样边界:注明是调试器暂停、汇编采样入口还是原始异常入口;这些时刻不能互换。
- 确定所属栈:当前执行依据 IPSR/CONTROL,异常帧依据原始 EXC_RETURN。
- 验证再算:先检查范围与序列;拒绝输入时不更新历史,不沿用上一次“有效数值”。
- 需要帧才核验帧:入口可信、无对应堆栈错误、支持的帧格式、足够长度、状态自洽;越界地址不解引用。
- 需要调用链才做展开:关联同构建指令/调试信息,保留不确定性;裸内存候选仅是候选。
- 写下一项反证实验:增加一个未覆盖调用点、换优化等级、引入受控异常嵌套,或用 ELF/Map 纠正边界。
6.2 模块接口的三类输出 #
| 输出 | 典型输入 | 调用者应该做什么 |
|---|---|---|
STACK_OK |
区间、身份、序列和 SP 满足支持条件 | 看 has_numbers/has_frame,只陈述本模型支持的量 |
STACK_INSUFFICIENT |
错栈/错构建、上下文未知、历史边界改变、旧序列、未支持的扩展帧 | 保留原始证据,补身份/上下文或换正确解析器,不假填数值 |
STACK_INVALID |
上下界倒置、SP 越界/未对齐、帧截断、非法编码 | 拒绝正常计算;排查输入与现场,不把状态码直接当根因 |
输入结构和输出结构不得互相重叠;每个非空指针必须指向本调用期间有效的真实对象。C 函数不能替调用者证明这些基础内存契约。
7. 嵌入式系统中的对应位置 #
7.1 栈诊断能力地图 #
| 层级 | 解决什么问题、数据/控制链路 | 本课与后续落点 |
|---|---|---|
| 基础 | 链接保留区 + 复位初始 MSP + 有效 SP -> 本次占用跨度与边界判断 | 本课深入;下一主题 map-file 交叉确认链接产物 |
| 进阶 | IPSR/CONTROL/EXC_RETURN 选择上下文;异常自动帧与软件压栈分层;采样历史保持最低观测 SP | 本课深入证据条件;异常细节沿用 L035,不重写 fault 课程 |
| 高级:优化与展开 | PC/LR + 同构建 ELF/DWARF + 指令语义 -> 有条件恢复调用者;内联/尾调用可能没有相应物理帧 | 本课用反例建立边界;在 map-file、host-test 中固定构建对照,不实现通用展开器 |
| 高级:填充、静态报告与防护 | 启动前受控填充 -> 执行路径 -> 停止后扫描;.su 函数级报告 -> 调用路径组合;MPU/栈限制 -> 访问/栈操作越界拦截,各测不同风险 |
本课给保守实验门槛;rtos-concurrency/task、memory 研究任务栈,performance-power-reliability/mpu、fault-containment 实做防护 |
| 跨外设联动 | 外设事件 -> IRQ(中断请求)-> HAL(Hardware Abstraction Layer,硬件抽象层)中断处理函数 -> 回调 -> 应用代码,嵌套会叠加主栈;DMA(直接存储器访问)完成事件可触发 IRQ,但搬运的数据缓冲区不是栈消耗量本身 | 这里只定义未来观测方案;到 rtos-concurrency/isr 比较 ISR 与任务栈,不为本课强加外设或猜回调 |
| 系列差异 | F4 的 M4F 基线 -> F7/H7 的 M7 内存/缓存观察 -> H5 的 M33 安全域与栈限制;不同芯片/内核必须换资料与采样器 | 见下表;核心依据分别为 [S1、S5、S6、S7] |
上表的高级能力不是“多几个名称”。例如栈保护解决的是及早发现某类越界,与事后读最低 SP 不同;必须把访问/异常信号、保护边界、handler 及记录路径连起来实验。本课不在缺少相应前置和资料配置时把这些能力硬塞进采样模块。
迁移清单:通用的是证据方法,不是地址与汇编。
| 参考系列 | 机制如何深化 | 必须重查/修改 | 后续可验证实验 |
|---|---|---|---|
| STM32F407xx / Cortex-M4F | 本课特权 Thread/MSP 的受控快照;浮点扩展帧主动拒绝 | 具体订货料号的 SRAM、CCM(内核耦合存储器)、启动/链接符号;不能把不连续 RAM 拼成一个栈区间 [S2、S3] | 对照向量首项、链接边界、两次快照 |
| STM32F7 / Cortex-M7 | MSP/PSP 的证据纪律仍可迁移;加入缓存、TCM(紧耦合存储器)与总线观察条件 | 具体 F7 的内存映射、缓存状态、调试可见性、工具与核心包;不能保证调试探针读到缓存中最新数据 [S5 的 memory/cache 章节] | cache 课比较内核读值与调试器读值,保留缓存状态;本课无测量结论 |
| STM32H5 / Cortex-M33 | 安全扩展涉及 Secure/Non-secure(安全/非安全)栈与异常上下文;栈限制寄存器扩大可观测/防护边界 | 当前安全状态、MSP/PSP 的安全域、MSPLIM/PSPLIM 支持和配置、目标 H5 手册与 CubeH5 头文件;不能只移植八字段汇编 [S6 的 stack、security extension、exception 章节] | task/memory 与 fault-containment 课在明确安全配置下比较不同栈/限制故障 |
| STM32H7,以 RM0433 覆盖的 H743 为对照 | 多总线/电源域、AXI SRAM 与 TCM 让“栈在哪里、何时可观察”变得更关键 | H743 的实际 SRAM 域、复位/供电、缓存及探针访问;RM0433 不代表全部 H7,更不能证明双核型号的同步行为 [S5、S7] | cache/mpu/low-power 课比较不同 RAM 中的快照及复位后可见性;双核另选对应手册 |
7.2 参考目标与完整时钟/复位链 #
7.2.1 资料与器件边界 #
参考器件为 STM32F407xx / Cortex-M4F,不选定开发板、完整订货料号、外部晶振、引脚或探针接线。架构依据 PM0214 Rev 10(2020-03);器件集成依据 RM0090 Rev 22(2026-05)与 DS8626 Rev 12(2026-03);HAL/LL 层级依据 UM1725 Rev 8(2023-03)。均已按本次可获取的文档封面核对,不是声称它们永远为最新版。
计划沿用 STM32CubeF4 v1.28.3 作为拟复现包边界。本次没有安装/解包该包,也没有从产品静态页取得这一补丁版本的发布记录,因此未核验其准确发布信息、CMSIS 子版本和实际启动模板。产品页 [S4] 仅支持包的定位;v1.28.3 不被伪装成已下载核对的包版本。目标开工前应核对实际包内 Release_Notes.html/包清单、CMSIS 头文件及启动文件,再决定是否继续使用该版本,并把结果写入目标实验记录。代码所依赖的内核寄存器行为由 [S1] 约束,不靠猜包补丁差异。
7.2.2 时钟路径不是“给栈开一个外设时钟” #
AHB(高级高性能总线)、APB(高级外设总线);HCLK 是本例的 AHB/内核标称时钟。
HSI(内部高速振荡源)或 HSE(外部高速时钟源,板级待填)
├─ 可直接选 SYSCLK(系统时钟)
└─ 可进入主 PLL(锁相环)
f_PLL_input = f_source / PLLM
f_VCO = f_PLL_input * PLLN
f_PLL_SYS = f_VCO / PLLP
↓ RCC_CFGR.SW 选择、SWS 反馈实际来源
SYSCLK
↓ AHB 预分频 HPRE
HCLK -> Cortex-M4 运行与相关 AHB/SRAM 访问路径
├─ APB1 预分频 PPRE1 -> PCLK1(本课无独立 APB1 外设)
└─ APB2 预分频 PPRE2 -> PCLK2(本课无独立 APB2 外设)上述为 F407 路径与字段,依据 RM0090 Rev 22 第 7 章针对 F405/407/415/417 的 RCC(复位与时钟控制)章节(p.214 起);PLL 公式见 §7.3.2,SW/SWS、HPRE/PPRE 见 §7.3.3。第 6 章对应 F42/F43,不可替代本课 F407 的器件依据。[S2 §7]
- 若实际选择 PLL,必须从实际板级时钟频率和 PLLM/N/P 推出 SYSCLK,再由 HPRE 推出 HCLK;同时检查允许频率、电源条件和 Flash 等待周期。这里不填写假想晶振和运行频率。
- 最终工作频率应在实验单记录为
HCLK = 实际 SYSCLK / HPRE;SystemCoreClock是软件维护的标称元数据,不是频率测量。示例保存 RCC 的 CR/PLLCFGR/CFGR 原始值辅助核对。 - **本课没有栈外设的内核时钟、预分频器、RCC 外设门控/复位位或 HAL 栈句柄。**不能为凑表格发明它们。SRAM 可访问条件按器件 RCC/存储器章节核对;本例不改动已启动系统的 SRAM 时钟策略。[S2 §2、§7]
- 示例由同步软件调用触发,不定义采样周期,也不利用定时器。若以后使用定时 IRQ 采样,必须另列该定时器的实际内核时钟与倍频条件、门控/复位、PSC/ARR,以及
T=(PSC+1)(ARR+1)/f_TIM,本课不拿 APB 时钟直接猜f_TIM。 - HCLK 不进入本课的字节差值公式;它会影响函数执行时间、异步事件介入机会以及“多久采到一次”。本课没有时序测量,不能由两个 SP 样本反推执行时间。
7.2.3 复位与启动链 #
上电/引脚/软件/看门狗等实际复位来源
-> 复位时钟及存储器启动映射(核对 BOOT 配置,不猜板级跳线)
-> 向量表首字载入初始 MSP,复位向量确定入口
-> 所选 startup / Reset_Handler
-> SystemInit、数据段初始化、C 运行时初始化(实际顺序查启动文件)
-> main
-> HAL_Init
-> CubeMX 生成并按真实板卡填写的 SystemClock_Config
-> StackProbe_Init -> StackDemo_RunOnce初始 MSP 的架构取值位置是启动映射下 0x00000000 的首字;后续 VTOR(向量表偏移寄存器)可能重定位向量表,不能把暂停时 VTOR 无条件当作复位最初映射。改变时钟、调试复位策略或重新加载程序,都会使采样属于不同时间段。本课快照只在 RAM 中,不跨复位保留,RCC_CSR 原始值读取不主动清复位标志;实际复位来源仍需结合工具操作记录判定。[S1 §2.1.3;S2 §2、§6]
7.3 CubeMX、HAL、CMSIS、LL 与寄存器对照 #
CMSIS(Cortex 微控制器软件接口标准)提供内核访问接口;LL(低层驱动)与 HAL(硬件抽象层)主要组织芯片外设访问。本课处理器栈寄存器属于 CMSIS/CPU 层,不是某个外设驱动。[S1 的 CMSIS 说明;S4、S12]
CubeMX/CubeMX2 配置清单(配置意图,未实际生成工程):
| 项目 | 本课明确的设置/约束 | 目标开工前的必填内容 |
|---|---|---|
| MCU Selector(器件选择) | 限定 STM32F407xx,不默认某块板 | 实际完整订货料号、封装、Flash/RAM 容量 |
| SYS/Debug | 选择 SWD(串行线调试)作为工程意图 | 板级连接、探针、供电/复位方式;本课不填引脚电路 |
| RCC/Clock Configuration | 用实际 HSI/HSE 与 PLL/AHB/APB 配置生成工程 | 时钟源、频率、分频、电源条件、Flash 等待周期;保留工程配置文件 |
| Middleware/外设 | 不新增定时器、DMA、UART 或 RTOS | 若现有工程已启用外设,登记它们是否影响采样,不擅改全局配置 |
| Firmware Package | 拟沿用 CubeF4 v1.28.3,下载/版本核对待执行 | 实际包版本、HAL/CMSIS 版本、生成器版本和启动模板 |
| 链接与启动文件 | 使用真实目标生成的脚本/启动文件 | RAM 区域、向量首项符号、所有静态段、保留栈大小;用实验明确选值,不填通用“推荐栈大小” |
| 工具链 | 目标参考代码含 GNU Arm 汇编/属性与 GNU ld 片段 | GNU Arm 编译器、汇编器、链接器、GDB 版本;其他工具链必须重写对应适配 |
HAL 系统初始化的最小调用顺序:
/* 放入实际 Cube 生成工程相应 USER CODE 区,不是可单独链接的工程。 */
#include "main.h" /* 由该目标工程真实生成 */
#include "stack_probe.h" /* 本课自有文件 */
#ifndef STUDY_FIRMWARE_TAG
#error "Define a build tag and map it to the full ELF hash in the experiment record"
#endif
volatile uint32_t g_stack_demo_status;
/* main 内:保留生成器的其他必要初始化,不复制一个假的 SystemClock_Config。 */
/*
if (HAL_Init() != HAL_OK) {
Error_Handler();
}
SystemClock_Config();
SystemCoreClockUpdate();
g_stack_demo_status = StackProbe_Init((uint32_t)STUDY_FIRMWARE_TAG);
if (g_stack_demo_status == STACK_OK) {
g_stack_demo_status = StackDemo_RunOnce();
}
*/这段是集成位置说明,不是注释掉代码后就有完整 main。实际工程必须保留生成的 main、SystemClock_Config、时基和 Error_Handler;只有第 9 节的本课模块作为新增编译单元。HAL_Init/HAL 的默认时基路径属于系统初始化;SystemClock_Config 是生成应用函数,不是 HAL 驱动内部栈接口。时钟配置失败应走生成工程的错误路径,不继续把未建立的频率标称写成实验条件。[S12 的 HAL configuration、HAL generic driver 与 RCC 章节]
| 配置对象/字段 | 所有者、生命周期 | 与本课有关的语义 |
|---|---|---|
RCC_OscInitTypeDef:OscillatorType、HSI/HSE 状态、PLL.PLLState/PLLSource/PLLM/PLLN/PLLP/PLLQ |
通常为生成的 SystemClock_Config 局部配置对象;不由采样模块持有 |
描述振荡源与 PLL;按真实器件/包头文件逐项复核,不为栈采样重新初始化 PLL |
RCC_ClkInitTypeDef:ClockType/SYSCLKSource/AHBCLKDivider/APB1CLKDivider/APB2CLKDivider |
同上;配置调用后对象本身不再是硬件状态 | 选择系统/总线来源和分频,需结合 SWS/HPRE/PPRE 读回核对 |
| HAL 外设句柄 | 不适用 | 无独立外设,故无 HAL_*_HandleTypeDef、DMA 句柄所有权或 HAL_*_...Callback |
g_stack_probe、history、bounds |
本课模块静态持有,从初始化到复位;main 单一写入路径 | 是诊断对象,不冒充 HAL 句柄 |
上表字段名的最终边界是所选 Cube 包头文件;本次没有编译该包,不能声称逐字段已经在 v1.28.3 验证。HAL 初始化顺序与层级由 [S12] 支持,具体包/生成器差异仍是目标开工门禁。
HAL → CMSIS/LL/关键寄存器对照:
| 操作意图 | 实际层与入口 | 关键字段/行为 | 明确不做什么 |
|---|---|---|---|
| 建立系统基础 | HAL_Init,生成工程时基 |
HAL 基础初始化与时基;可能引入 SysTick(系统滴答)异常 | 不宣称它预留/保护了本课栈区 |
| 建立系统时钟 | 生成的 SystemClock_Config 调用 RCC HAL |
RCC_CR、RCC_PLLCFGR、RCC_CFGR 的来源/分频状态 [S2、S12] | 不用 SystemCoreClock 代替硬件状态或频率测量 |
| 读两种 SP | CMSIS __get_MSP/__get_PSP 的意图;本课汇编 MRS |
处理器 MSP/PSP;不是内存映射外设寄存器 [S1] | 不发明 LL/HAL 栈采样 API |
| 判活动栈/模式 | __get_CONTROL、__get_IPSR;汇编保存同名寄存器 |
CONTROL.SPSEL/nPRIV、IPSR 异常号 [S1] | 不拿当前 handler MSP 充当 Thread 的入口帧地址 |
| 有限屏蔽可配置异常 | 保存 PRIMASK、cpsid i、恢复 PRIMASK |
缩小普通 IRQ 对采样序列的扰动,但 NMI/HardFault 不因此被屏蔽 [S1] | 不声称得到原子全系统快照;不调用 HAL 来改 SP |
| 查看异常帧 | 原始入口 EXC_RETURN + 支持的帧副本 | 核心保存字、对齐、故障有效性 [S1] | 本课目标采样器不安装 fault handler,也不盲读 SP |
| 查看初始 MSP | 启动映射/向量首项 + _estack 等实际符号 |
启动和链接契约,不是 HAL 配置字段 [S1、S2] | 不把改一个 _Min_Stack_Size 数值当成保留区与运行保护全完成 |
**IRQ/DMA 回调:不适用,栈采样没有独立外设 HAL 回调。**保留生成工程已有的系统时基中断链即可;不要新增、替换或重复定义 SysTick_Handler。如果已有 IRQ 在两次采样间运行,它可能造成未被捕获的更低 MSP;本次 observed_peak_bytes 不覆盖那个未采样峰值。
8. 主机实验与硬件迁移边界 #
8.1 Host Fake 验证轨道:先运行最小例 #
在仓库根目录用已安装的主机 GCC 执行,输出目录选 .state/tmp/L036-stack-evidence(临时且不进入课程发布)。这是主机编译器,不是 GNU Arm 编译器。
New-Item -ItemType Directory -Force .state/tmp/L036-stack-evidence | Out-Null
$src = 'lessons/L036-stack-evidence/host'
$out = '.state/tmp/L036-stack-evidence'
gcc -std=c11 -Wall -Wextra -Werror -pedantic -O0 -g `
"$src/stack_evidence.c" "$src/minimal.c" -o "$out/minimal.exe"
if ($LASTEXITCODE -ne 0) { throw 'minimal build failed' }
& "$out/minimal.exe"
if ($LASTEXITCODE -ne 0) { throw 'minimal run failed' }
foreach ($opt in @('O0', 'O2')) {
gcc -std=c11 -Wall -Wextra -Werror -pedantic "-$opt" `
"$src/stack_evidence.c" "$src/test_stack_evidence.c" -o "$out/test-$opt.exe"
if ($LASTEXITCODE -ne 0) { throw "test build failed: $opt" }
& "$out/test-$opt.exe"
if ($LASTEXITCODE -ne 0) { throw "test run failed: $opt" }
}最小例的确定性输出:
used=0 remaining=1024 observed_peak=0
used=128 remaining=896 observed_peak=128
used=64 remaining=960 observed_peak=128测试不使用 -DNDEBUG,否则 assert(断言)会被移除。完整用例文件是 host/test_stack_evidence.c,验证如下:
| 输入/反例 | 预期接受/拒绝及不变量 |
|---|---|
| 空栈、加深后回浅、恰在 low | 正确计算;历史最低值不回退;剩余 0 不改写为安全 |
| SP 在区间外/不对齐、空区间/倒置区间 | 输入无效,无无符号回绕,历史不变 |
| MSP/PSP 错配、构建不符、未知上下文 | 证据不足;不能继续显示旧数值为新结果 |
| 改边界继续用旧历史、重复或倒序 sequence | 拒绝混合证据;清零历史必须由调用方明确执行 |
| 破坏历史内部值、空指针 | 拒绝;输出指针为空时仅能返回状态 |
| 接近 UINT32_MAX 的数字区间 | 仍先验证后相减,不把主机真实地址塞进 32 位 MCU 地址模型 |
| 基本帧截断、aligner 需要第九字、错误状态/PC/EXC_RETURN | 拒绝解释;浮点扩展帧报“不支持”,不误读基本帧 |
| MSP/PSP 基本帧与 Handler 返回帧 | 支持条件下接受,但只表示必要条件通过 |
**本次主机证据(2026-10-20):**主机 GCC 16.2.0,-std=c11 -Wall -Wextra -Werror -pedantic;最小例输出如上,完整测试在 -O0 与 -O2 下各通过 149 项断言。可复核命令、输出、退出码及源文件哈希见仓库运行记录 .state/runs/20261020-203304522-42772/teacher-validation.log,复现脚本为同目录 teacher-check.py。该记录不是硬件验证证据。
主机运行只证明这份数据模型的判断逻辑;不证明 ARM ABI、真实异常帧、实际链接范围、汇编代码、总线可读性或目标最坏峰值。
8.2 STM32 参考实现轨道:可操作但未执行 #
8.2.1 先建立真实保留区,不猜一个栈大小 #
本课新增的文件都是自写教学代码,不包含完整厂商驱动、启动文件或完整第三方工程:
host/stack_evidence.h, stack_evidence.c 可迁移判断模块
host/minimal.c, test_stack_evidence.c 只编译进主机程序
stm32/stack_probe.h, stack_probe.c 目标快照与生命周期
stm32/stack_probe_entry.S 无压栈的 GNU Arm 采样入口
stm32/stack_demo.c 有限深度实验调用链
stm32/stack_reservation.ld.inc 需合并的 GNU ld 片段,不是完整链接脚本实际目标工程只加入 stack_evidence.c、stack_probe.c、stack_demo.c、.S;include 路径加入本课 host 与 stm32。不要把两个主机 main 文件一起编进目标。
链接步骤:
- 选真实 MCU 的生成脚本,核对
MEMORY的RAM是否为预定连续可用主 SRAM,而非 CCM、别的内存域或拼接区。 - 找实际向量首项符号、RAM 静态段、启动清零范围、堆/栈保留段。符号名
_estack、_ebss是本片段的集成契约;若实际工程不同,必须同步改片段和对应检查,不创建一个同名但无真实约束的假符号。 - 本课只给该工程增加一个专属保留段。将片段合入实际
SECTIONS,替代生成脚本原有的堆/栈占位段方案,避免双重保留。本任务没有执行上述工程修改。 - 由目标工程显式提供
STUDY_STACK_BYTES(链接符号),根据构建/已知调用路径和异常预算选实验值;这里不写一个能误当通用建议的数字。保留_estack与专属段高端一致,并核对向量首项实际引用它。 - 明确本实验无动态分配;若生成工程/库仍使用堆,必须为它保留独立且不重叠的真实区间。不能因为本课没调用
malloc就把未知库的堆需求忽略。 - 看实际链接错误、Map、段列表和 ELF 符号,确认所有 RAM 段与栈不重叠。
NOLOAD仅是段属性;检查启动代码究竟清哪些范围。片段本身不提供运行时越界保护。[S10]
若实际生成脚本只有 _Min_Stack_Size 占位检查,应核对它是否确实排除了其他段对主栈区的使用:本模块需要能命名的实际区间,不能只根据一个最小尺寸符号宣称整个高端区都已排除其他用途。后续 map-file 课会深入产物交叉确认。
8.2.2 采样器的支持条件与时机 #
- 必须在有足够安全栈余量的特权 Thread/MSP 中调用,初始化在系统时钟建立之后。
StackProbe_Capture是 C 函数,有自己的编译器帧;它再调用不压栈的汇编入口。记录的是汇编入口时的 MSP,已经包括 wrapper 与调用链的用量,不是业务调用者进入 wrapper 之前的 SP。- 汇编不使用额外栈,也不调用 C;仅临时使用 AAPCS(Arm 架构过程调用标准)允许调用者视为可破坏的 R1~R3,保存两种 SP、CONTROL、IPSR、原 PRIMASK、返回 LR、PC 锚点和活动 SP;不改写须由被调用者保留的寄存器。[S14 §4.4]PRIMASK 只在短窗口屏蔽可配置异常,恢复原值;不是永久关闭中断。[S1]
- 这不是原子的全寄存器快照。NMI/HardFault 仍可能打断,RCC 元数据在 C 侧稍后读取;这些异常路径不得调用此采样器或改快照对象。发生故障时回到 L035 的专用采集路径。
- 采样器不读
*MSP、不写栈填充图案、不改 MSP/PSP/CONTROL,不安装 HardFault,也不强迫 PSP 初始化。 Init/Capture的条件检查本身已经是 C 代码,可能消耗栈;它们不是栈已溢出时的救命入口。低余量系统不能期待“先进入诊断 C 函数再发现溢出”足够安全。
若需要业务函数“调用前”SP,用调试器在调用指令前暂停并独立保存那组现场。不要把其读数与本例汇编入口的读数当成同时发生。
8.2.3 数据生命周期、错误路径与验证材料 #
bounds/history/sequence 由模块静态持有,StackProbe_Init 开始新一轮;g_stack_probe_raw 只为汇编与 C 交换本次数据;g_stack_probe 保存发布的快照,main 是唯一写者。状态为 EMPTY -> WRITING -> COMPLETE;调试器在 Capture 返回之后观察。volatile(易变限定)和 DMB(数据存储屏障)不等于并发读者拿到了原子完整快照,也不解决其他系列的缓存一致性。[S1 的 DMB;S5]
Init拒绝非法区间/模式时返回STACK_INVALID,禁用后续采样。- Capture 前置条件不符或序列耗尽时返回
STACK_INSUFFICIENT,不产生新快照;此前 COMPLETE 数据仍是历史。必须同时检查本次返回值与 sequence,不把旧快照当新结果。 - 若已经采样但诊断拒绝输入,快照仍以 COMPLETE 发布原始值与拒绝原因,
has_numbers=0。COMPLETE 只表示写入完成,不表示诊断有效。 - API 不接收外部 writer,不支持 ISR、RTOS 多任务并发调用或 fault 重入。
目标实验记录至少保留:完整订货料号、板卡/探针/复位方式、生成器与 Cube/HAL/CMSIS 版本、编译/链接选项、ELF 与完整 SHA-256、链接脚本、Map、反汇编、原始寄存器、快照、时钟链计算和操作顺序。缺少这些材料时不把“看到了个数”升级为目标验证结论。
9. 最小代码示例与完整参考模块 #
本节先给声明/算法/调用,再给目标适配。公共算法是 C11、公开整数字段为固定宽度、无动态内存;CMSIS 内联/目标汇编和 GNU 属性是明确的平台适配边界,不宣称 ISO C11 能读 CPU 栈寄存器。所有目标文件仍是“参考实现,待目标工程编译和上板验证”。
五个动作在这里有具体落点:声明在头文件;实现在算法与目标 .c/.S;实例化是 Host 的局部 history 与目标模块的静态对象;组装由目标工程提供真实链接符号、构建身份和生成初始化;使用是 main 调用初始化及一次实验。它们不是新的架构课,也不引入函数指针/依赖注入框架。
9.1 接口声明:调用者持有输入、历史和输出 #
文件:host/stack_evidence.h。 依赖仅为 <stdint.h>;字段中的地址是 MCU 数字模型,不是可解引用许可。历史须以 {0} 初始化。
#ifndef STACK_EVIDENCE_H
#define STACK_EVIDENCE_H
#include <stdint.h>
#define STACK_OK UINT32_C(0)
#define STACK_INSUFFICIENT UINT32_C(1)
#define STACK_INVALID UINT32_C(2)
#define STACK_MSP UINT32_C(1)
#define STACK_PSP UINT32_C(2)
#define STACK_REASON_NONE UINT32_C(0)
#define STACK_REASON_ARGUMENT UINT32_C(1)
#define STACK_REASON_BOUNDS UINT32_C(2)
#define STACK_REASON_CONTEXT UINT32_C(3)
#define STACK_REASON_IDENTITY UINT32_C(4)
#define STACK_REASON_SP UINT32_C(5)
#define STACK_REASON_HISTORY UINT32_C(6)
#define STACK_REASON_FRAME UINT32_C(7)
#define STACK_REASON_UNSUPPORTED UINT32_C(8)
#define STACK_REASON_STACKING UINT32_C(9)
typedef struct {
uint32_t low, high; /* [low, high); high is a valid EMPTY SP only. */
uint32_t stack_id, build_tag;
} StackBounds;
typedef struct {
uint32_t sp, stack_id, build_tag, sequence, context_known;
} StackSample;
typedef struct {
uint32_t initialized;
StackBounds bounds;
uint32_t min_sp, last_sequence;
} StackHistory;
typedef struct {
uint32_t status, reason, has_numbers;
uint32_t used_bytes, remaining_bytes, observed_peak_bytes;
} StackReport;
typedef struct {
uint32_t exc_return, cfsr, text_low, text_high, word_count;
uint32_t words[9]; /* Copied input; NEVER an arbitrary SRAM pointer. */
} StackBasicFrameInput;
typedef struct {
uint32_t status, reason, has_frame;
uint32_t resume_pc, saved_lr, xpsr, occupied_bytes;
} StackFrameReport;
uint32_t StackEvidence_Observe(const StackBounds *bounds,
const StackSample *sample, StackHistory *history, StackReport *report);
uint32_t StackEvidence_BasicFrame(const StackBounds *bounds,
const StackSample *entry, const StackBasicFrameInput *input,
StackFrameReport *report);
#endif9.2 算法实现:先验证,后做减法,最后提交历史 #
文件:host/stack_evidence.c。 无硬件依赖,不持有调用者对象。错误路径清空输出有效性且不更改历史;帧函数只操作已经复制的定长对象。支持基本帧的依据为 [S1],它不是展开器。
#include "stack_evidence.h"
#include <stddef.h>
static uint32_t fail(StackReport *r, uint32_t status, uint32_t reason)
{
r->status = status;
r->reason = reason;
return status;
}
static uint32_t valid_bounds(const StackBounds *b)
{
return b->low < b->high && ((b->low | b->high) & UINT32_C(3)) == 0U
&& (b->stack_id == STACK_MSP || b->stack_id == STACK_PSP);
}
static uint32_t same_bounds(const StackBounds *a, const StackBounds *b)
{
return a->low == b->low && a->high == b->high
&& a->stack_id == b->stack_id && a->build_tag == b->build_tag;
}
uint32_t StackEvidence_Observe(const StackBounds *b, const StackSample *s,
StackHistory *h, StackReport *r)
{
uint32_t min_sp;
if (r == NULL) { return STACK_INVALID; }
*r = (StackReport){0};
if (b == NULL || s == NULL || h == NULL) {
return fail(r, STACK_INVALID, STACK_REASON_ARGUMENT);
}
if (!valid_bounds(b)) { return fail(r, STACK_INVALID, STACK_REASON_BOUNDS); }
if (s->context_known != 1U) {
return fail(r, STACK_INSUFFICIENT, STACK_REASON_CONTEXT);
}
if (s->stack_id != b->stack_id || s->build_tag != b->build_tag) {
return fail(r, STACK_INSUFFICIENT, STACK_REASON_IDENTITY);
}
if (s->sp < b->low || s->sp > b->high || (s->sp & 3U) != 0U) {
return fail(r, STACK_INVALID, STACK_REASON_SP);
}
if (s->sequence == 0U || h->initialized > 1U) {
return fail(r, STACK_INVALID, STACK_REASON_HISTORY);
}
if (h->initialized == 1U) {
if (!same_bounds(b, &h->bounds)) {
return fail(r, STACK_INSUFFICIENT, STACK_REASON_HISTORY);
}
if (h->min_sp < b->low || h->min_sp > b->high
|| (h->min_sp & 3U) != 0U || h->last_sequence == 0U) {
return fail(r, STACK_INVALID, STACK_REASON_HISTORY);
}
if (s->sequence <= h->last_sequence) {
return fail(r, STACK_INSUFFICIENT, STACK_REASON_HISTORY);
}
}
min_sp = h->initialized == 0U || s->sp < h->min_sp ? s->sp : h->min_sp;
/* All validation precedes state changes and unsigned subtraction. */
h->bounds = *b;
h->min_sp = min_sp;
h->last_sequence = s->sequence;
h->initialized = 1U;
r->status = STACK_OK;
r->has_numbers = 1U;
r->used_bytes = b->high - s->sp;
r->remaining_bytes = s->sp - b->low;
r->observed_peak_bytes = b->high - min_sp;
return STACK_OK;
}
static uint32_t frame_fail(StackFrameReport *r, uint32_t status, uint32_t why)
{
r->status = status;
r->reason = why;
return status;
}
static uint32_t handler_number_possible(uint32_t n)
{
/* Cortex-M4 architecture-level filter, not a device IRQ availability test. */
return (n >= 2U && n <= 6U) || n == 11U || n == 12U || n == 14U
|| n == 15U || (n >= 16U && n <= 255U);
}
uint32_t StackEvidence_BasicFrame(const StackBounds *b, const StackSample *s,
const StackBasicFrameInput *f, StackFrameReport *r)
{
StackHistory history = {0};
StackReport report;
uint32_t expected_stack, bytes, mode;
if (r == NULL) { return STACK_INVALID; }
*r = (StackFrameReport){0};
if (f == NULL) { return frame_fail(r, STACK_INVALID, STACK_REASON_ARGUMENT); }
if (StackEvidence_Observe(b, s, &history, &report) != STACK_OK) {
return frame_fail(r, report.status, report.reason);
}
switch (f->exc_return) {
case UINT32_C(0xFFFFFFF1):
case UINT32_C(0xFFFFFFF9):
case UINT32_C(0xFFFFFFFD): break;
case UINT32_C(0xFFFFFFE1):
case UINT32_C(0xFFFFFFE9):
case UINT32_C(0xFFFFFFED):
return frame_fail(r, STACK_INSUFFICIENT, STACK_REASON_UNSUPPORTED);
default: return frame_fail(r, STACK_INVALID, STACK_REASON_FRAME);
}
expected_stack = (f->exc_return & 4U) != 0U ? STACK_PSP : STACK_MSP;
if (s->stack_id != expected_stack) {
return frame_fail(r, STACK_INSUFFICIENT, STACK_REASON_IDENTITY);
}
/* MMFSR[5:3] and BFSR[5:3]: unstack/stack/lazy-state errors. */
if ((f->cfsr & UINT32_C(0x00003838)) != 0U) {
return frame_fail(r, STACK_INSUFFICIENT, STACK_REASON_STACKING);
}
if (b->high - s->sp < 32U || f->word_count < 8U || f->word_count > 9U) {
return frame_fail(r, STACK_INVALID, STACK_REASON_FRAME);
}
bytes = (f->words[7] & (UINT32_C(1) << 9)) != 0U ? 36U : 32U;
mode = f->words[7] & UINT32_C(0x1FF);
if (b->high - s->sp < bytes || f->word_count < bytes / 4U
|| (bytes == 36U && (s->sp & 7U) != 0U)
|| (f->words[7] & (UINT32_C(1) << 24)) == 0U
|| (f->exc_return == UINT32_C(0xFFFFFFF1) ? !handler_number_possible(mode) : mode != 0U)
|| f->text_low >= f->text_high
|| ((f->text_low | f->text_high) & 1U) != 0U
|| (f->words[6] & 1U) != 0U || f->words[6] < f->text_low
|| f->words[6] >= f->text_high) {
return frame_fail(r, STACK_INVALID, STACK_REASON_FRAME);
}
r->status = STACK_OK;
r->has_frame = 1U;
r->resume_pc = f->words[6];
r->saved_lr = f->words[5]; /* No claim that this is a normal call return. */
r->xpsr = f->words[7];
r->occupied_bytes = bytes;
return STACK_OK;
}9.3 最小使用:三个样本即可观察到峰值不回退 #
文件:host/minimal.c。 主机测试入口持有所有对象,生命周期覆盖三次同步调用;printf 仅属于主机,不进入目标采样路径。
#include "stack_evidence.h"
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
int main(void)
{
const StackBounds bounds = {0x20001000U, 0x20001400U, STACK_MSP, 7U};
const uint32_t sequence[] = {0x20001400U, 0x20001380U, 0x200013C0U};
StackHistory history = {0};
StackReport report;
uint32_t i;
for (i = 0U; i < 3U; ++i) {
const StackSample s = {sequence[i], STACK_MSP, 7U, i + 1U, 1U};
assert(StackEvidence_Observe(&bounds, &s, &history, &report) == STACK_OK);
printf("used=%" PRIu32 " remaining=%" PRIu32 " observed_peak=%" PRIu32 "\n",
report.used_bytes, report.remaining_bytes, report.observed_peak_bytes);
}
return 0;
}9.4 目标声明与静态快照契约 #
文件:stm32/stack_probe.h。 目标适配层依赖公共算法。App 只调用 API,不写全局快照;快照由模块持有至复位。
#ifndef STUDY_STACK_PROBE_H
#define STUDY_STACK_PROBE_H
#include "stack_evidence.h"
#include <stdint.h>
#define STACK_PROBE_EMPTY UINT32_C(0)
#define STACK_PROBE_WRITING UINT32_C(1)
#define STACK_PROBE_COMPLETE UINT32_C(2)
typedef struct {
uint32_t msp, psp, control, ipsr, primask, return_lr, pc_anchor, active_sp;
} StackProbeRaw;
typedef struct {
uint32_t state, status, build_tag, sequence;
uint32_t hclk_nominal_hz, rcc_cr, rcc_pllcfgr, rcc_cfgr, rcc_csr;
StackBounds bounds;
StackProbeRaw raw;
StackReport report;
} StackProbeSnapshot;
/* One privileged Thread-mode producer; debugger reads only after completion. */
extern volatile StackProbeSnapshot g_stack_probe;
uint32_t StackProbe_Init(uint32_t build_tag);
uint32_t StackProbe_Capture(void);
uint32_t StackDemo_RunOnce(void);
#endif9.5 汇编采样入口:不额外压栈,但不是调用前 SP #
文件:stm32/stack_probe_entry.S。 GNU Arm/Thumb 的平台代码,通过固定偏移写模块私有 raw 对象。没有 push/pop、不调用 C、不遍历栈;PC 是 label 锚点。PRIMASK/MSP/PSP/CONTROL/IPSR 语义见 [S1];调用约定中 R0~R3 的暂存用途见 [S14 §4.4],实际 GNU Arm ABI 选项仍须按目标工程确认。
/* Original teaching code. GNU assembler, Cortex-M4/Thumb only.
* Not a fault/exception entry and not a universal register snapshot.
*/
.syntax unified
.cpu cortex-m4
.thumb
.text
.align 2
.global StackProbe_Entry
.type StackProbe_Entry, %function
.thumb_func
StackProbe_Entry:
mrs r3, PRIMASK
cpsid i
ldr r2, =g_stack_probe_raw
.Lsample:
mrs r1, MSP
str r1, [r2, #0]
mrs r1, PSP
str r1, [r2, #4]
mrs r1, CONTROL
str r1, [r2, #8]
mrs r1, IPSR
str r1, [r2, #12]
str r3, [r2, #16]
str lr, [r2, #20]
adr r1, .Lsample
str r1, [r2, #24]
mov r1, sp
str r1, [r2, #28]
dmb
msr PRIMASK, r3
bx lr
.size StackProbe_Entry, .-StackProbe_Entry9.6 目标实现:真实链接区间到判断模块 #
文件:stm32/stack_probe.c。 模块拥有静态区间、历史与快照,main 注入 build_tag;链接器注入两个边界符号。字段偏移由静态断言约束。使用 CMSIS/器件头文件,等待选定 Cube 包逐项编译核对。
/* Reference only: pending compilation in the selected STM32CubeF4 project. */
#include "stack_probe.h"
#include "stm32f4xx_hal.h"
#include <stddef.h>
#include <stdint.h>
extern uint8_t __study_stack_low__[];
extern uint8_t __study_stack_high__[];
extern void StackProbe_Entry(void);
/* Link-visible to assembly; not a public mutable application object. */
volatile StackProbeRaw g_stack_probe_raw;
volatile StackProbeSnapshot g_stack_probe;
static StackBounds bounds;
static StackHistory history;
static uint32_t initialized;
static uint32_t sequence;
_Static_assert(sizeof(uintptr_t) == 4U, "STM32F407 address width required");
_Static_assert(sizeof(StackProbeRaw) == 32U, "assembly layout mismatch");
_Static_assert(offsetof(StackProbeRaw, msp) == 0U, "msp offset");
_Static_assert(offsetof(StackProbeRaw, psp) == 4U, "psp offset");
_Static_assert(offsetof(StackProbeRaw, control) == 8U, "control offset");
_Static_assert(offsetof(StackProbeRaw, ipsr) == 12U, "ipsr offset");
_Static_assert(offsetof(StackProbeRaw, primask) == 16U, "primask offset");
_Static_assert(offsetof(StackProbeRaw, return_lr) == 20U, "lr offset");
_Static_assert(offsetof(StackProbeRaw, pc_anchor) == 24U, "pc offset");
_Static_assert(offsetof(StackProbeRaw, active_sp) == 28U, "sp offset");
uint32_t StackProbe_Init(uint32_t build_tag)
{
uint32_t low = (uint32_t)(uintptr_t)__study_stack_low__;
uint32_t high = (uint32_t)(uintptr_t)__study_stack_high__;
/* Policy: this module deliberately supports privileged Thread/MSP only. */
if (__get_IPSR() != 0U || (__get_CONTROL() & 3U) != 0U
|| low >= high || ((low | high) & 7U) != 0U) {
initialized = 0U;
return STACK_INVALID;
}
bounds = (StackBounds){low, high, STACK_MSP, build_tag};
history = (StackHistory){0};
sequence = 0U;
g_stack_probe = (StackProbeSnapshot){0};
g_stack_probe.build_tag = build_tag;
g_stack_probe.bounds = bounds;
initialized = 1U;
return STACK_OK;
}
uint32_t StackProbe_Capture(void)
{
StackProbeRaw raw;
StackSample sample;
StackReport report;
uint32_t result;
if (initialized == 0U || __get_IPSR() != 0U || (__get_CONTROL() & 3U) != 0U
|| sequence == UINT32_MAX) {
return STACK_INSUFFICIENT; /* No new snapshot; previous one stays historical. */
}
g_stack_probe.state = STACK_PROBE_WRITING;
StackProbe_Entry(); /* Its MSP includes this function's existing C frame. */
raw = g_stack_probe_raw;
++sequence;
sample = (StackSample){raw.active_sp, STACK_MSP, bounds.build_tag, sequence,
raw.ipsr == 0U && (raw.control & 3U) == 0U && raw.active_sp == raw.msp};
result = StackEvidence_Observe(&bounds, &sample, &history, &report);
g_stack_probe.raw = raw;
g_stack_probe.report = report;
g_stack_probe.status = result;
g_stack_probe.sequence = sequence;
/* Nominal metadata, not measured timing and not an atomic RCC snapshot. */
g_stack_probe.hclk_nominal_hz = SystemCoreClock;
g_stack_probe.rcc_cr = RCC->CR;
g_stack_probe.rcc_pllcfgr = RCC->PLLCFGR;
g_stack_probe.rcc_cfgr = RCC->CFGR;
g_stack_probe.rcc_csr = RCC->CSR; /* Read only; do not clear reset flags. */
__DMB();
g_stack_probe.state = STACK_PROBE_COMPLETE;
return result;
}9.7 有限深度的目标调用:观察点而非帧大小承诺 #
文件:stm32/stack_demo.c。 应用实验层只依赖目标公开接口。pad 的 64 个字节不等于函数总帧大小;编译器还可能保存寄存器和对齐。noinline(禁止内联)是本实验采用的 GNU 属性,只限制内联,不冻结帧布局或禁止所有其他优化;实际结果看反汇编。[S13 的 noinline 条目]
/* GNU Arm target experiment, not generic C11 frame-layout code. */
#include "stack_probe.h"
static volatile uint32_t demo_sink;
__attribute__((noinline)) static uint32_t sample_leaf(void)
{
volatile uint32_t pad[16];
uint32_t i, status;
for (i = 0U; i < 16U; ++i) { pad[i] = i + 1U; }
status = StackProbe_Capture();
/* Work after capture, to discourage a sibling/tail call in this experiment. */
for (i = 0U; i < 16U; ++i) { demo_sink ^= pad[i]; }
return status;
}
uint32_t StackDemo_RunOnce(void)
{
uint32_t first = StackProbe_Capture();
uint32_t second;
if (first != STACK_OK) { return first; }
second = sample_leaf();
return second;
}9.8 链接保留区片段:尺寸必须由目标构建明确选择 #
文件:stm32/stack_reservation.ld.inc。 这不是完整脚本,也不是 Cube 默认脚本的已验证替代品。MEMORY/NOLOAD 的基础语义见 [S10];ASSERT、ORIGIN/LENGTH 表达式与实际工具链版本兼容性仍须在目标工程核对。不得原样粘贴到未知位置后宣称链接/保护完成。
/* Integration FRAGMENT, not a standalone linker script.
* Place inside the actual GNU ld SECTIONS block, after allocated RAM sections.
* Replace the generated heap/stack-reservation SECTION, not the whole script.
* STUDY_STACK_BYTES must be deliberately selected for the actual build.
* RAM, _ebss and _estack must match the ACTUAL linker/startup symbols.
*/
.study_stack (ORIGIN(RAM) + LENGTH(RAM) - STUDY_STACK_BYTES) (NOLOAD) :
{
__study_stack_low__ = .;
. = . + STUDY_STACK_BYTES;
__study_stack_high__ = .;
} > RAM
ASSERT(DEFINED(STUDY_STACK_BYTES), "STUDY_STACK_BYTES must be selected")
ASSERT(STUDY_STACK_BYTES > 0 && STUDY_STACK_BYTES <= LENGTH(RAM), "bad stack size")
ASSERT((__study_stack_low__ & 7) == 0 && (__study_stack_high__ & 7) == 0,
"stack bounds must be 8-byte aligned")
ASSERT(_ebss <= __study_stack_low__, "BSS overlaps study stack")
ASSERT(_estack == __study_stack_high__, "vector initial MSP must match stack high")10. 常见错误与纠正方式 #
| 错误 | 为什么不成立 | 本课的纠正动作 |
|---|---|---|
| 把所有 SRAM 当栈 | 还可能有静态对象、堆或不连续存储区 | 使用专属保留段与对应符号,查 Map/向量/ELF |
| 从未验证的无符号减法得到“大剩余量” | SP 越界可能发生回绕 | 先做顺序/对齐/身份检查,再减法 |
| 在错栈中累计最低 SP | Handler 的 MSP 与 Thread 的 PSP 不属于同一条历史 | 每个真实栈/会话独立 history;未知身份报证据不足 |
| 一次最低 SP 就称峰值 | 采样外可能更深,尤其是 IRQ/库路径 | 改称“最低已观测”,补覆盖路径和采样时刻 |
| 扫描未改图案就是空闲栈 | 未写局部区、巧合图案、复用旧字都会影响扫描 | 同时保留 SP 观测与覆盖条件,不写成最坏保证 |
| 拿普通函数帧当八字异常帧 | 八字格式来自异常入口硬件,不是通用 C 布局 | 查原始 EXC_RETURN 和入口 SP;普通帧看机器码/展开信息 |
| handler 调了几个 C 函数后再读 LR/SP 当入口值 | 编译器和嵌套异常可能改变现场 | 在入口专用采集路径保存,或承认不是入口证据 |
pc_anchor 就是任意故障 PC |
本课值是采样 label,不是被打断指令 | 字段名明确来源,不把它用于故障根因定位 |
COMPLETE 就是有效/本次结果 |
写入完成不等于输入有效;拒绝前置条件会保留旧样本 | 同看本次返回值、sequence、status、has_numbers |
有 -g 或帧指针就必能完整回溯 |
编译、优化和展开器支持各有边界 | 保存完整选项与同构建信息,检查实际指令/表,而非承诺 |
| 用更大栈“修好”后就结案 | 可能掩盖越界写、错指针或异常嵌套问题 | 保留原始证据,提出能区分假设的实验 |
10.1 为什么不提供 main 内直接 memset 栈的代码 #
当前 main、C 运行时和中断可能正在使用同一区域。把整个保留区填成图案会破坏活动帧和返回信息。填充实验只能在已核对的启动/调试阶段,对明确未被使用、已从其他段排除的范围执行,并处理随后的初始化与异常;本任务不修改启动文件,也不提供可误粘贴执行的危险写栈示例。[S1 的 stack/exception;本课支持范围]
若将来做真实填充高水位实验,最低门槛为:
- 在实际启动文件中确定执行填充的阶段及当时 MSP;确认未用 C 函数隐式压栈到要填的区域。
- 指明哪些范围正在活跃、哪些范围允许填充;明确异常/复位/调试器是否可能介入。
- 核对
.bss/.data/保留段的后续初始化,避免图案被启动代码改写后误算。 - 执行记录中说明覆盖的输入、函数和异常路径;停止写入后再扫描合法范围。
- 同时保留最低 SP、图案扫描结果与编译信息;承认未写区域、图案巧合与未覆盖路径的局限。
本课主机测试里的 test_observation_limits 用 16 字节普通数组演示两类漏计,不对真实栈做写入。
11. 调试观察点 #
11.1 目标调试前的产物门禁(均待执行) #
- 用实际工程构建;保存编译、汇编、链接器版本和完整命令行。优先建立一份容易观察的构建和一份原生产优化构建,不混用 ELF。
- 可为实验构建评估
-Og -g3 -fstack-usage -fno-omit-frame-pointer -fno-optimize-sibling-calls;先由实际 GNU Arm 版本--help/配套手册确认,再保留项目生成的 CPU、浮点和 ABI 选项。不可为了本课随意改浮点 ABI。[S8、S9] -fstack-usage产生的是函数级.su信息,含static/dynamic/bounded等限定,不是整条调用链的自动最坏峰值。动态且无 bounded 的记录尤其不能直接当总上界;组合函数路径还需考虑同时存活帧与异常。[S9]- 保留同一构建的 ELF、Map、链接脚本、
.su、反汇编和 SHA-256。不要拿新代码解释旧快照。 - 检查
StackProbe_Entry没有被错误当作主机源处理、没有额外压栈;检查 C wrapper 的真实帧;核对调用链未意外变成预期外的内联/跳转。
下列只是用户在真实目标构建完成后的产物查看示例;本次未执行,$elf 必须选择真实构建产物,不是假文件名:
$elf = (Resolve-Path -LiteralPath (Read-Host '输入本次实际构建的 ELF 路径')).Path
Get-FileHash -Algorithm SHA256 -LiteralPath $elf
arm-none-eabi-nm -n $elf
arm-none-eabi-objdump -d -S $elf
arm-none-eabi-readelf -S $elf具体 binutils 工具参数须与目标安装版本核对;这里不把主机 GCC 或主机 ld 的成功当作这些目标工具已执行。
11.2 暂停在哪里,比“能看寄存器”更重要 #
| 观察点 | 应保存的东西 | 为什么选这里 |
|---|---|---|
| 初始化完成、第一次 Capture 前 | 向量首项、实际区间符号、寄存器、RCC/频率计算、ELF 身份 | 把静态与初始假设先锁定 |
StackProbe_Entry 入口 |
这次暂停时的 SP/LR/PC、CONTROL/IPSR(工具若可见) | 与编译后的 C wrapper 帧配对,不误当业务调用前 |
| Capture 返回之后 | state=COMPLETE、sequence、status、raw、report |
避免在 WRITING 中途把一半字段当完整快照 |
sample_leaf 采样及返回后 |
第二次快照、实际反汇编、.su、调用位置 |
对比深入调用路径;不要求差值恰好为 pad 的 64 字节 |
| 任意现有 IRQ 路径 | handler 内观察与原始入口采集路径的区分 | 验证“当前 MSP”与“原始异常入口 SP”不是同一概念 |
GDB(GNU 调试器)的基础查看命令示意:
info registers sp lr pc
p/x &__study_stack_low__
p/x &__study_stack_high__
p/x g_stack_probe
bt
info frame
disassemble StackProbe_Entry命令基线参考 GNU 官方手册存档 [S11];实际目标 GDB、远程调试服务器、寄存器描述与展开能力仍须用该版本 help 和官方手册核对。若工具未暴露 $msp/$psp,不要杜撰寄存器名字或把 $sp 同时叫 MSP 和 PSP;可先观察模块实际保存的两个字段。
**bt 显示的是当前暂停现场的展开,不是自动对 g_stack_probe 中过去的快照做离线展开。**把程序从采样点运行到 main 再看 bt,不能说那是刚才 leaf 内的调用链。若要保留那时的链,应在同一采样边界暂停并导出原始证据;离线重建需要额外工具支持,本课不实现。
11.3 构建、连接、用户烧录与观察步骤 #
本任务不执行烧录,也不改 IDE/CubeMX 全局设置。以下是将来由用户自行确认硬件后执行的参考流程,而非本次运行事实:
- 在独立目标工程内选准 MCU、供电/调试连接和包版本,保存工程配置;构建失败先处理真实诊断,不套用主机测试结果。
- 使用调试器只读连接,确认目标身份和复位方式;不认识芯片或连接失败时停止,不尝试擦除/解除保护。
- 若用户决定加载固件,先按既有工程流程备份必要数据,再在其选定的 IDE/探针工具中选择本次 ELF 和已核对的 Flash 布局,执行该工程自己的下载/校验操作并保存日志。本课不生成通用擦除/写入地址命令。
- 按 11.2 的位置设置断点,从可复现的复位/启动条件运行,导出两次快照及寄存器。暂停会改变实时行为;不要用它证明无中断扰动的实时峰值。
- 对照边界、sequence 和构建身份填写结果;若首次都无法确认,不继续跑“压力实验”。
12. 实战实验 #
实验 A:主机的有效样本与拒绝路径 #
**输入:**8.1 的最小例和完整测试。**操作:**按命令编译运行 O0/O2,再阅读拒绝用例。**验收:**两种优化下所有断言通过;三个确定输出符合表格;拒绝样本不改变历史且新 report 不保留有效数值。
**反证:**把源码里的边界检查与减法顺序交换,或把历史最低值直接设为每次当前 SP。能否由现有用例抓住?只在复制出的个人实验分支/文件上做这种故意破坏,不改本课归档源码作为“修复”。
实验 B:观测不到的峰值与填充反例 #
阅读 test_observation_limits。序列只注入 0x1380 -> 0x13C0;设想两次间实际到过 0x1100,算法没有收到这次样本,所以不会凭空生成 768 字节峰值。
再看 16 字节图案数组:概念上分配至索引 4,只改写索引 12~15,扫描只能看到高端 4 字节改变。写入与图案相同值时可能完全看不出。
**验收:**分别写出“采样峰值”“图案改写范围”“假设中的最深分配”三个量,解释为何不相等。该数组实验不是测量主机真实栈,也不是测量 STM32 栈。
实验 C:基本异常帧的拒绝边界 #
使用 test_frames 的已复制数组:依次检查 F9、FD、F1 的栈/模式要求,再观察扩展编码、非法编码、CFSR 堆栈错误、截断、错误 xPSR/PC、对齐字不足的输出。
**验收:**能说明为何 SP == high 可作为空栈样本,却绝不能承载八字帧;为何“支持条件自洽”依然不能证明输入来自真实异常。没有写任何裸 SRAM 读循环。
实验 D:目标双采样(待实际目标工程执行) #
**前置门禁:**8.2 的真实链接保留区、正确主栈、足够实验余量、所选包/工具链与构建身份齐全。**操作:**按 7.3 集成,分别在第一次 Capture 返回后和 leaf 中第二次 Capture 返回后暂停并导出快照;必要时修改个人目标工程观察位置,不改归档课程。
**预期但未测得:**在非内联的受控路径中,第二次快照可以体现更深调用的跨度;实际差值由机器码/保存寄存器/对齐决定,不承诺固定字节数。observed_peak_bytes 不应比同历史内已接受样本的 used_bytes 更小,且同历史中不递减。
**拒绝条件:**链接段不闭合、向量首项不匹配、包版本未核实、目标编译失败、采样不在支持模式、出现故障、无法确认 ELF 身份。停止并保存失败证据,不用手填“正常”数值补齐表格。
实验 E:优化与回溯的交叉检查(待执行) #
在实际目标工程中分别构建易调试版本和原生产优化版本。每个版本各保存 ELF、选项、反汇编、.su 和暂停寄存器,独立重新初始化历史。比较有无内联/尾调用及回溯中的差异;没有出现差异也是有效结果,不强行生成预期截图。
**验收:**结论引用具体指令/帧证据,不是“优化开关越低就必定更真实”。不能跨版本累计高水位、用一个版本 ELF 解读另一个版本 PC,或把软件工具的回溯当作未损坏栈的证明。[S8、S9、S11]
**建议实验记录格式(空值表示待用户填写,不是已完成):**目标身份;时钟/复位条件;构建哈希及 tag 对应;真实区间;采样边界;原始 SP/CONTROL/IPSR/PC/LR;是否原始异常入口及 EXC_RETURN;判定与不支持项;下一次反证。记录放入独立目标实验材料,而不是回填本课 hardware_verified=true。
13. 自检题 #
先独立回答,再对照提示;生成本节不会自动提高掌握度。
low=0x20001000, high=0x20001400, sp=0x20001280时,占用跨度与距下界各多少?若此前采过0x20001200,已观测峰值是多少?- 为什么
sp==high可通过 Observe,却不能成为有效基本帧的入口? - Handler 中读到的当前 MSP,为什么不一定指向被中断 Thread 的自动帧?
- 采样器拒绝新调用时,调试器仍看到上一次 COMPLETE 快照,怎样避免误当本次结果?
- 同一
stack_id但低边界改变,为什么不直接把旧 min_sp 与新边界组合? - 给出一个填充扫描低估栈分配深度的具体例子。
- 普通 C 函数的 LR 与异常入口 EXC_RETURN 有什么本质不同?
- 为什么
-fno-omit-frame-pointer或一张完整bt截图都不能单独证明调用链可靠? - 本课目标快照中的
pc_anchor与一个原始异常帧中的 PC,各能回答什么问题? - 什么证据到位后,你才愿意把一句“可能栈溢出”改成更精确的结论?
核对提示(不是掌握度评定)
- 当前跨度
0x180=384,距下界0x280=640;已观测峰值0x200=512字节。仍非最坏峰值证明。 - high 是开区间端点;以它为 SP 的空栈没有可供该区间内读取的八字帧。
- Thread 可能原用 PSP,且 handler 的 C 函数/嵌套异常也可能改变当前 MSP;需要原始入口信息。
- 检查本次返回值和 sequence 是否推进,COMPLETE 只表示某次写完。
- 区间与构建是历史身份的一部分;明确开启新历史而不是静默混合。
- 预留 64 字节仅写 8 字节,或写入值与填充相同。
- 普通 LR 与调用返回相关;入口 EXC_RETURN 编码返回模式、栈和帧类型,不能当普通 Flash 地址解析。
- 优化、损坏、工具支持与构建匹配仍有条件;帧指针开关也非对所有函数的保证。
- pc_anchor 定位本课采样代码;帧 PC 属于被打断的有限上下文,故障语义还需状态/指令互证。
- 至少应能锁定真实栈区、构建、采样时机/所属栈、原始现场和可区分其他原因的反证实验,而不是只有一个 SP。
14. 面试题 #
问:裸机系统的栈怎么定大小?
答题方向:先给具体构建/启动/区间约束,再组合函数级静态信息、可能同时存在的调用链、异常嵌套、库路径和受支持的浮点上下文预算,以运行覆盖补充验证并保留裕量依据。一次采样或图案扫描只能提供部分证据,不能给通用“某数字就够”的答案。
问:HardFault 时看到 SP 越界,就一定是栈溢出吗?
答题方向:这支持“该候选 SP 不满足该区间”,还要排除错栈、错误链接界、损坏 SP、错误固件解释等;若堆栈故障位已置位,不应盲读八字帧。解释已经保存的原始状态,别先修改现场。
问:为什么调用栈里没有某个源代码函数?
答题方向:区分源级调用、物理帧与工具展示,检查内联、尾调用、帧指针/展开信息、构建匹配和栈损坏;缺少一帧不是单一根因的证明。[S8、S11]
问:为什么不能让栈监控函数自己证明“还有足够栈供我运行”?
答题方向:进入 C wrapper 本身已经使用栈;检查晚于部分风险。常规观测入口和越界后的安全故障入口属于不同契约,本课只实现前者。
问:从 F407 迁到 H5/H7,要改哪些?
答题方向:保留“身份/边界/时机/构建互证”,重新查内核、安全域、栈限制/异常编码、真实 RAM 与缓存/调试可见性、启动链接和 Cube/工具版本,不能只换一个头文件。[S5、S6、S7]
15. 延伸思考 #
- 如果主循环每秒采一次,最深 IRQ 只持续几微秒,为什么增加采样日志不必然覆盖它?用哪个明确时机的新样本或专门实验更能反驳当前判断?
- 若同一数值地址在两版 ELF 中都位于
.text,怎样证明你选的是正确版本?考虑完整哈希与调试下载日志,而非 32 位 tag 本身。 - 一个合法基本帧的保存 LR 也可能是 EXC_RETURN:它提示了什么上下文层次,又为什么不足以拼出全部嵌套链?
- H5 的安全/非安全栈、H7 的缓存与内存域会分别挑战“地址值正确”“该地址可读”“读到的是同一时刻数据”哪一环?先列待查资料与实验,不凭 F4 经验填答案。
- 以后增加 RTOS 时,任务 PSP 历史和 handler MSP 历史如何分离、何时归属到特定任务?只写对象与证据需求,留到
task/memory课程实现。
16. 本节总结 #
一个可信的栈判断至少同时说清楚:哪个构建、哪个栈、哪个区间、哪个时刻、哪种现场、哪条可反驳的结论。
本课两个核心概念贯穿同一证据闭环:
- 静态保留、瞬时 SP、最低已观测 SP、填充改写范围与最坏使用量不可互换。
- PC/LR、异常硬件帧、软件帧和工具回溯不可互换,必须与同构建产物互证。
主机已验证的是判断模型;目标参考代码没有对应构建/板级证据。未实现的能力被明确拒绝,尤其是浮点扩展帧、通用展开、并发调用和 fault 安全入口,而不是用未核对的猜测补齐。
17. 下一步 #
下一主题严格为 debugging-verification / map-file:用真实链接产物、段区间、符号和向量首项交叉确认本课栈边界,不把“链接脚本写了某个数”当作区间已证明。
随后按既有路线进入 logic-analyzer、host-test、unit-test;任务栈归属与并发留到 rtos-concurrency/task、memory、isr;缓存、MPU 和故障隔离分别留到 performance-power-reliability/cache、mpu、fault-containment。这些是能力地图的后续落点,不是本课新增概念或已完成能力。
Reviewer 通过后仅更新课程审查状态与按路线授课的进度:本主题记入 completed,下一主题为 map-file;[mastery] 保持不变。未修改既有课程、.ai/、templates/ 或 AGENTS.md。用户实验/答案到位后,再按系统流程更新掌握度证据。
18. 参考资料 #
正文 [S编号] 与 lesson.json.sources 的编号一一对应。以下是本课选择的版本边界,不作“当前最新”保证。访问日期统一为 2026-10-20。ST 下载文档采用实际成功访问的完整官方地址;GNU 为项目官方资料,但按本仓库 primary_domains 规则不计入 primary 数量。
-
**[S1] ST PM0214 Rev 10,2020-03,STM32 Cortex-M4 MCUs and MPUs programming manual。**重点:§2.1.2/2.1.3 的栈、处理器寄存器;§2.3.7、Figure 12、Table 18 的异常帧与返回;§4.4.7 CCR 与 §4.4.10~4.4.13 CFSR 的对齐及堆栈错误;MRS/MSR、DMB 与异常屏蔽说明。
https://www.st.com/resource/en/programming_manual/dm00046982-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics.pdf -
**[S2] ST RM0090 Rev 22,2026-05,STM32F405/415、F407/417、F427/437、F429/439 reference manual。**重点:§2 系统/存储器、§7 针对 F405/407/415/417 的 RCC、debug support。它规定器件集成,不替代处理器栈规则。
https://www.st.com.cn/resource/en/reference_manual/dm00031020-stm32f405415-stm32f407417-stm32f427437-and-stm32f429439-advanced-armbased-32bit-mcus-stmicroelectronics.pdf -
**[S3] ST DS8626 Rev 12,2026-03,STM32F405xx / STM32F407xx datasheet。**重点:器件范围、内存/容量、功能框图、时钟与电气边界;实际订货料号仍须补齐。
https://www.st.com.cn/resource/en/datasheet/stm32f407vg.pdf -
**[S4] ST STM32CubeF4 官方产品页,2026-10-20 页面快照。**只支持 CubeF4 软件包定位;计划拟用 v1.28.3,实际补丁发布信息与包内容未核验,不作为具体 CMSIS 子版本/模板的证据。
https://www.st.com/en/embedded-software/stm32cubef4.html -
**[S5] ST PM0253 Rev 6,2026-05,STM32F7/STM32H7 Cortex-M7 processor programming manual。**重点:处理器内存模型、缓存/TCM 和异常;这里只支撑迁移检查项,不从它推导所有具体 F7/H7 RAM 地址。
https://www.st.com.cn/resource/en/programming_manual/dm00237416-stm32f7-series-and-stm32h7-series-cortexm7-processor-programming-manual-stmicroelectronics.pdf -
**[S6] ST PM0264 Rev 8,2026-03,STM32 Cortex-M33 MCUs and MPUs programming manual。**重点:栈/栈限制、安全扩展与异常;H5 的器件安全配置需再查具体目标手册,不在此拼出所谓通用 H5 采集器。
https://www.st.com.cn/resource/en/programming_manual/pm0264-stm32-cortexm33-mcus-programming-manual-stmicroelectronics.pdf -
**[S7] ST RM0433 Rev 9,2026-10,STM32H742/H743/H753/H750 reference manual。**重点:系统/存储器、时钟/电源域和复位,仅作为选定 H7 子线的对照;不代表双核或全部 H7。
https://www.st.com.cn/resource/en/reference_manual/dm00314099-stm32h742-stm32h743-753-and-stm32h750-value-line-advanced-armbased-32bit-mcus-stmicroelectronics.pdf -
**[S8] GNU GCC 15.2.0 Manual,Optimize Options。**重点:frame-pointer、inline、sibling-call 优化的条件与限制;不是本次主机 GCC 或尚未选定的 GNU Arm 工具链的版本声明。
https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc/Optimize-Options.html -
**[S9] GNU GCC 15.2.0 Manual,Developer Options。**重点:
-fstack-usage的函数级输出及 static/dynamic/bounded 限定。
https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc/Developer-Options.html -
**[S10] GNU 官方 ld 文档存档,Command Language / MEMORY / Optional Section Attributes。**URL 位于
ld-2.9.1目录,但本次缓存正文标题标记为ld version 2, January 1994,不把路径名称当作已核实的正文版本。只用于稳定的区间安排、符号/段与 NOLOAD 基础说明;不拿旧存档证明现代工具所有表达式、ASSERT 或目标工程兼容性。目标脚本必须以实际 GNU ld 手册与构建诊断核验。
https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html -
**[S11] GNU 官方 GDB 手册存档(页面生成日期 2002-02-14),Stack frames / Backtrace / Frame Info。**仅作基础帧/命令语义依据,不对现代 GDB 或 Cortex-M 远程目标的展开实现作保证。当前在线手册本次访问返回 403;现代实现待选定目标工具后核对。
https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_42.html
同一存档的相邻阅读位置:node 41(Frames)、node 44(Frame Info)。 -
**[S12] ST UM1725 Rev 8,2023-03,Description of STM32F4 HAL and low-layer drivers。**重点:HAL 初始化/配置、generic driver、RCC 和 HAL/LL 分工;具体 v1.28.3 包头文件/生成器模板仍待目标开工核验。
https://www.st.com.cn/resource/en/user_manual/dm00105879-description-of-stm32f4-hal-and-ll-drivers-stmicroelectronics.pdf -
**[S13] GNU GCC 15.2.0 Manual,Common Function Attributes,noinline 条目。**用于说明目标示例的禁止内联属性及其优化边界,不作实际 GNU Arm 版本声明。
https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc/Common-Function-Attributes.html -
**[S14] Arm Compiler v5.06 for µVision armlink User Guide,DUI0377H,2017,§4.4(pp.4-9~4-10)。**该官方资料中的 AAPCS 调用约定说明支持 R0~R3 的参数/结果/暂存用途和调用者保存责任;只用于这项 ABI 基础,不宣称本课选择了 Arm Compiler 5 或已验证 GNU Arm 的生成代码。
https://documentation-service.arm.com/static/5f4e134dca7b6a3399376bd3
**证据纪律:**资料对行为提供约束,主机测试对模型提供证据,目标构建对具体代码集成提供证据,板级观察才对真实行为提供证据。这四层不能互相替代。未取得的包版本、目标编译、调试或测量结果均留为未验证,不在正文或元数据中补写成功。