Cortex-M 启动流程与内存布局(从复位到 main)
上电到 main 之间发生了什么,是理解一切"玄学问题"(启动失败、栈溢出、代码搬移、优化错乱)的基础。本文按时间线串起星球的系列答疑。
一、上电第一拍:硬件取栈顶,BootROM 接管
复位后硬件自动把向量表第 [0] 项(initial_stack,初始栈顶地址)加载进 MSP——不需要任何软件参与;随后从向量表 [1] 取复位向量开始执行。取栈顶和第一条指令是在 BootROM 中完成的:BootROM 依据 BOOT0/BOOT1 引脚配置,去特定位置加载栈顶并执行复位语句 | Jack,老师,在第一步过程中,取栈顶指针到msp中,是在谁的指导下完成。
理解取指路径还要知道总线分工:I-Code Bus 专门取指令(从 Flash 或映射到 0 地址的 SRAM,送译码后进 ALU,运算更新 LR/PC),D-Code Bus 专门取数据(如 Flash 中的 const 常量读入通用寄存器,但不包括 PC/LR)| Jack,I-Code-Bus, D-Code-Bus有什么区别?它们在如何取指令和数据。
二、栈的方向:满减栈
Cortex-M 用满减栈(Full Descending):"满"指 SP 始终指向有效数据的栈地址——若不先移动指针就写入,会破坏栈中已有数据;"减"指 SP 向低地址方向移动。对应的"空栈"则 SP 指向空位,可以先写后移 | Jack,老师,满减栈什么意思? 满意。栈到底预留多大、在哪设置,由链接器的堆栈配置决定 | Jack,Linker Setting中;栈水位的粗测手段之一是直接打印栈指针 | Jack,栈水位的直接打印栈指针来检测。
三、__main:C 运行时在 main 之前做的事
很多人以为栈是在 __main 里初始化的——其实到能执行 __main 时调用栈早就可用了(否则函数调用、保存返回地址都无从谈起)。__main(ARMCC)或 __libc_init_array(GCC/Newlib)真正做的是:把 .data 从 Flash 复制到 RAM、清零 .bss、C++ 工程调用全局构造,最后跳进用户的 main()。栈只是链接脚本静态预留的一块内存;想做 0xA5A5A5A5 填充测水位是你自己在启动文件里做的事。堆则不同:边界符号(_end 等)早已确定,但分配器要到第一次 malloc 才建立索引结构 | Jack,关于堆栈和__main()在什么时候初始化。
四、异常返回与 Handler/Thread 切换
复位后 CPU 先处于 handler 模式,LR 被装入 0xFFFFFFF9 这类"伪返回值"——它不是内存地址,而是异常返回行为的编码(0xFFFFFFF9 表示返回 thread 模式并使用 MSP)。启动代码 LDR R0, =__main 后 BX R0 跳转,执行完毕靠 LR 中的编码自动切回 thread 模式运行 main | Jack,在复位异常后,CPU进入handle模式,LR被赋值0xFFFFFFF9。同理,SVC 这类软件触发的异常需要软件用 BX R14 显式返回,而大多数中断由硬件自动完成异常返回 | Jack,老师,这里的BX R14是指啥,通过异常返回值退出异常。写 PC 分支时数值必须保持 LSB=1(Thumb 状态),写成 0 会被当作企图切换 ARM 模式而触发 fault | Jack,老师,我不太能理解这里的含义。
五、段与地址的确定时机
- 全局变量地址在链接阶段确定:.o 里只有相对本文件的偏移,链接器按链接脚本生成绝对地址 | Jack,全局变量的地址,是在编译阶段还是链接阶段确定。
- ZI 段包含未初始化/清零的全局静态变量,且把堆区和栈区也算了进去;个别初始化为 0 的变量也可能被放进 RW 段,分析 map 文件时要注意 | Jack,ZI_data 不仅包含 未初始化:初始化为0的全局和静态变量、在map文件中有。
- 用分散加载(scatter)把函数定位到 RAM 是常见手法,启动时涉及从 Flash 的搬移 | Jack,老师,再请教一个问题。bootloader启动app会对app进行验签。
- 栈的 8 字节对齐由 AAPCS 约定,但编译器会在函数不用栈时省去 padding——对比实验要多看反汇编 | 花烛想迟到,Q: 在网上看到 AAPCS 标准要求函数和中断入口时。
六、加速启动的思路
快速启动有两条路:其一,把数据直接编进 hex 由 Flash 加载并借 cache 提速;其二,对 RAM 中的函数指针挂载点,用 DMA 在系统启动时批量填充 RAM,尽快达到系统就绪状态 | Jack,飞秒启动起始它分两种方式: 1。
小结
启动链条可以浓缩为:硬件装 MSP → BootROM 定入口 → __main 搬段清段 → 异常返回切 thread → main。凡是"改一行代码就死机/好了"的现象,都值得回到这条链上检查:地址是否合法(预取指)、栈是否溢出、段是否被搬到位。
来源笔记
- 老师,在第一步过程中,取栈顶指针到msp中,是在谁的指导下完成 | Jack
- I-Code-Bus, D-Code-Bus有什么区别?它们在如何取指令和数据 | Jack
- 老师,满减栈什么意思? 满意 | Jack
- Linker Setting中 | Jack
- 栈水位的直接打印栈指针来检测 | Jack
- 关于堆栈和__main()在什么时候初始化 | Jack
- 在复位异常后,CPU进入handle模式,LR被赋值0xFFFFFFF9 | Jack
- 老师,这里的BX R14是指啥,通过异常返回值退出异常 | Jack
- 老师,我不太能理解这里的含义 | Jack
- 全局变量的地址,是在编译阶段还是链接阶段确定 | Jack
- ZI_data 不仅包含 未初始化:初始化为0的全局和静态变量 | Jack
- 在map文件中有 | 像大海一样(答:Jack)
- 老师,再请教一个问题。bootloader启动app会对app进行验签 | jac
- Q: 在网上看到 AAPCS 标准要求函数和中断入口时 | 花烛想迟到
- 飞秒启动起始它分两种方式: 1 | Jack