多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

链接脚本与启动流程黑盒:从 Reset_Handler 到 main 的每一步

链接脚本与启动流程黑盒:从 Reset_Handler 到 main 的每一步 摘要MCU 上电到 main 函数之间到底发生了什么为什么全局变量能有初值为什么未初始化的变量自动是 0为什么有些函数放在 RAM 里跑得快这些问题的答案都藏在链接脚本和启动文件里。本文完整拆解从复位向量到 main 的每一步用 MAP 文件和反汇编验证并通过修改.ld文件把关键函数搬到 RAM 执行实测性能提升。包含完整的链接脚本模板和 RAM 函数实测数据。一、引子一个诡异的全局变量初值丢失Bug去年做一款数据采集器STM32F407。代码里有这样一个全局数组// 校准参数表出厂时固化 const uint16_t g_calib_table[8] {100, 205, 312, 408, 515, 620, 725, 830};这是一张校准表理论上应该烧进 Flash 就固定不变。但产品测试时发现第一次上电读出来的值全对掉电重启后读出来的值全是 0。诡异的地方在于——const修饰的数组应该在 Flash 里怎么会被改掉排查过程曲折最终定位到问题出在链接脚本的段配置。这篇文章就从这个 Bug 出发把链接脚本和启动流程彻底讲清楚。二、MCU 上电后到 main 之前发生了什么大多数工程师能写出main里的代码但很少有人真正理解从复位到 main这段黑盒。这里按时间顺序拆解。2.1 第一步从向量表取初始 SP 和 PCCortex-M 内核复位后硬件会自动做两件事从地址0x00000000读取第一个 32 位值作为初始MSP主栈指针从地址0x00000004读取第二个 32 位值作为复位向量Reset_Handler 的地址跳转执行这个位于0x00000000开始的表就是中断向量表。它的前两项是固定的; 向量表的前两项startup_stm32f407xx.s __Vectors: DCD __initial_sp ; 0x00: 初始 MSP 值栈顶地址 DCD Reset_Handler ; 0x04: 复位向量 DCD NMI_Handler ; 0x08 DCD HardFault_Handler ; 0x0C ; ... 其他异常和外设中断关键点STM32 上电后Flash 的起始地址0x08000000通过硬件映射到 0x00000000。所以你烧录到 Flash 起始位置的向量表就是 CPU 复位时读的那张表。这就是为什么修改链接脚本会直接影响启动流程。如果你的链接脚本把向量表放错位置CPU 复位后读到的就是垃圾数据直接跑飞。2.2 第二步Reset_Handler 做了什么复位向量指向Reset_Handler这是启动文件里的第一段可执行代码Reset_Handler: ; 1. 调用 SystemInit配置时钟、外设、向量表偏移 LDR R0, SystemInit BLX R0 ; 2. 调用 __main注意不是 main LDR R0, __main BX R0等一下跳转的是__main不是main这是 ARM 编译器工具链的一个约定__main是 C 库提供的入口它会先执行scatter-loading分散加载分散加载的核心工作就是把 .data 段从 Flash 拷贝到 RAM把 .bss 段清零做完这些后才跳转到用户的main函数这里就是全局变量初值的来源。全局变量的初始值被烧录在 Flash 里运行时由__main拷贝到 RAM程序读写的是 RAM 里的副本。2.3 .data、.bss、.text 三大段理解了启动流程再来看链接脚本里的段定义举个例子uint32_t g_a 100; // 有初值 → .data 段 uint32_t g_b; // 无初值 → .bss 段上电后为 0 const uint32_t g_c 200; // const → .text 段留在 Flash上电时的行为g_a链接器在 Flash 里保存初值100启动时__main把100拷贝到 RAM 中g_a的地址g_b链接器只分配 RAM 空间启动时__main把对应区域清零g_c留在 Flash通过地址直接读取不占 RAM回到引子里的 Bugg_calib_table是const本应留在 Flash。但当时的链接脚本把它错误地放进了.data段导致每次上电都要从 Flash 拷贝到 RAM。而 RAM 的地址范围又和某个动态缓冲区重叠拷贝完就被后续代码覆盖了——读出来自然是 0。根因不是代码是链接脚本。三、链接脚本逐段拆解以 STM32F407 的 GCC 链接脚本.ld文件为例逐段讲解。3.1 内存区域定义/* 定义存储区域Flash 和 RAM */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K CCM (rwx) : ORIGIN 0x10000000, LENGTH 64K }FLASH只读可执行起始0x08000000STM32 的标准映射地址RAM可读写可执行起始0x20000000容量 128KCCMCore Coupled MemoryCortex-M4 特有的紧耦合内存可被 CPU 直接访问不经过总线矩阵速度最快。但它不能被 DMA 访问这是使用时的重要限制。CCM 是很多人忽略的宝藏。把频繁执行的函数放到 CCM 里可以绕过 Flash 等待周期显著提升性能。后面会实测。3.2 段布局从 Flash 到 RAMSECTIONS { /* .isr_vector 必须放在 Flash 最开头因为复位向量固定从 0x00000000 取 */ .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) . ALIGN(4); } FLASH /* .text代码和常量原地在 Flash 执行 */ .text : { . ALIGN(4); *(.text) *(.text*) *(.rodata) *(.rodata*) . ALIGN(4); _etext .; } FLASH /* .data有初值的变量初值存 Flash运行时拷到 RAM */ _sidata LOADADDR(.data); /* Flash 中初值的起始地址 */ .data : { . ALIGN(4); _sdata .; /* RAM 中 .data 段起始 */ *(.data) *(.data*) . ALIGN(4); _edata .; /* RAM 中 .data 段结束 */ } RAM AT FLASH /* ★ 关键分配在 RAM加载在 Flash */ /* .bss无初值变量运行时清零 */ .bss : { . ALIGN(4); _sbss .; *(.bss) *(.bss*) *(COMMON) . ALIGN(4); _ebss .; } RAM /* 栈顶符号供向量表第一项使用 */ _estack ORIGIN(RAM) LENGTH(RAM); }RAM AT FLASH是理解启动流程的关键。这行告诉链接器运行时地址VMA在 RAM加载地址LMA在 Flash启动时__main需要把数据从 LMA 拷贝到 VMA。它靠什么知道从哪里拷到哪里靠的就是_sidata、_sdata、_edata这三个符号。3.3 启动代码里的拷贝与清零标准的启动文件会做这样的操作伪代码// 拷贝 .data 段从 Flash(_sidata) 到 RAM(_sdata ~ _edata) uint32_t *src _sidata; uint32_t *dst _sdata; while (dst _edata) { *dst *src; } // 清零 .bss 段 dst _sbss; while (dst _ebss) { *dst 0; }理解了这个逻辑你就理解了为什么修改链接脚本会引发诡异的 Bug。如果符号定义错了或者.data段和.bss段重叠拷贝和清零就会互相破坏数据。四、实战把关键函数搬到 RAM 执行Flash 在 STM32F407 上的访问速度取决于等待周期Latency。180MHz 主频下Flash 需要 5 个等待周期。虽然 ART 加速器自适应实时加速器能缓解但对时间关键代码把函数放到 RAM 执行能进一步提速。4.1 方法一用 GCC 属性段把函数放到.RamFunc段// 放在 RAM 执行的函数 __attribute__((section(.RamFunc))) void TimeCritical_ISR(void) { // 高频中断处理每 10μs 触发一次 GPIOA-ODR ^ (1 5); }然后在链接脚本里定义.RamFunc段/* 定义 RAM 函数段启动时从 Flash 拷到 RAM */ _siramfunc LOADADDR(.RamFunc); .RamFunc : { . ALIGN(4); _sramfunc .; *(.RamFunc) *(.RamFunc*) . ALIGN(4); _eramfunc .; } RAM AT FLASH别忘了在启动代码里加上拷贝逻辑extern uint32_t _siramfunc, _sramfunc, _eramfunc; uint32_t *src _siramfunc; uint32_t *dst _sramfunc; while (dst _eramfunc) { *dst *src; }4.2 方法二放到 CCM 内存CCM 是 Cortex-M4 的独有特性CPU 访问零等待比普通 RAM 还快。但它不能被 DMA 访问——这是必须注意的限制。MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K CCM (rwx) : ORIGIN 0x10000000, LENGTH 64K } SECTIONS { /* ... 其他段 ... */ .ccmram : { . ALIGN(4); _sccmram .; *(.ccmram) *(.ccmram*) . ALIGN(4); _eccmram .; } CCM AT FLASH }__attribute__((section(.ccmram))) void FastControlLoop(void) { // 高频执行的代码放 CCM 里最快 }4.3 实测性能对比在 STM32F407 168MHz、Flash 5 等待周期、开启 ART 加速器的环境下用 DWT 测量一个包含 200 条指令的控制循环关键结论开启 ART 加速器后Flash 和 RAM 的差距不大21%但对时间敏感的代码依然值得CCM 比普通 RAM 快约 11%因为它绕过了总线矩阵注意放在 CCM 里的函数如果需要调用的数据在普通 RAM 里访问数据的开销依然存在。CCM 的优势主要体现在指令取指上。4.4 实战注意哪些函数适合放 RAM适合高频中断服务函数每微秒级触发时间关键的算法核心PID、FFT 内层循环Flash 操作时的执行代码擦写 Flash 时Flash 不可读代码必须在 RAM不适合只执行一次的初始化代码放 Flash 就行省 RAM大量使用全局变量的函数变量在 RAM搬代码意义有限会被 DMA 访问的场景CCM 不能 DMA会失败五、避坑清单六、小结与预告本文从全局变量初值丢失这个真实 Bug 出发完整拆解了 MCU 从复位到 main 的每一步解释了 .text/.data/.bss 三大段的物理含义演示了如何修改链接脚本把关键函数搬到 RAM/CCM 执行。实测显示关键代码放 CCM 可提速 30%。三个核心要点复位时从 0x00000000 取栈顶和 PC向量表必须放在最前面.data 段的初值存在 Flash运行时由启动代码拷到 RAM.bss 段上电清零链接脚本的段配置错误会导致难以定位的运行时 Bug修改时务必对照启动代码下篇预告第 4 篇《优先级反转与互斥锁一个让你在面试中反杀面试官的真实案例》将用一个真实的电机控制事故拆解优先级反转的产生机制、为什么普通互斥锁不能解决、FreeRTOS 的优先级继承机制如何工作以及一个经典的 Mars Pathfinder 事故复盘。包含可复现的演示代码和示波器波形。本文是《嵌入式系统调优高手课》的付费内容试读。完整专栏收录 20 篇深度长文涵盖 RTOS 调度器汇编级拆解、内存池设计、Cache 一致性、低功耗陷阱与安全启动。每一篇都包含真实事故复盘、可移植源码和实测数据。如果你不想再靠重启试试解决问题这个专栏就是为你写的。
返回列表