ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32链接脚本.ld文件详解:从内存布局到启动运行的底层原理

STM32链接脚本.ld文件详解:从内存布局到启动运行的底层原理 1. 项目概述为什么一个.ld文件能决定MCU程序能不能跑起来你手头有一块STM32F103C8T6最小系统板烧进去的固件一上电就跑飞、串口没输出、LED不闪——调试器连上一看PC指针停在0x00000000或者0xFFFFFFF9这种明显非法地址。这时候别急着换芯片、查电源、重焊晶振先打开你的工程目录找到那个叫stm32f103c8t6.ld或类似名字的文件。它可能只有几百行甚至被IDE自动生成后藏在某个build子目录里但就是这个不起眼的文本文件决定了你的代码从哪里开始执行、全局变量放在哪片RAM里、堆栈空间有多大、中断向量表落点是否正确。ld脚本不是可有可无的配置项它是链接阶段的“宪法”是MCU内存布局的唯一法定蓝图。我在江科大带学生做智能门锁课程设计时连续三届都有人卡在这一步Keil或IAR能编译通过、能生成hex但烧录后完全无响应。最后发现全是ld脚本里.data段的加载地址LOADADDR和运行地址ADDR写反了导致初始化代码把零初始化数据段.bss当成了有效代码去执行。这背后没有玄学只有对MCU硬件资源的精确映射——STM32F103C8T6的Flash从0x08000000开始共64KBSRAM从0x20000000开始共20KB而它的启动流程强制要求复位向量必须位于Flash起始偏移0x00处。ld脚本就是把编译器产出的零散目标文件.o按照这片物理内存的硬性约束像拼图一样严丝合缝地组装起来。它解决的不是“怎么写代码”的问题而是“代码最终在芯片里住哪间房、走哪条走廊、用哪部电梯”的底层基建问题。适合谁看如果你正在用Keil、IAR或GCC如arm-none-eabi-gcc开发STM32哪怕只是照着例程改个GPIO翻转只要想搞懂为什么SystemInit()必须在main()之前执行、为什么malloc分配的内存总比你预期的小、为什么FreeRTOS的堆空间总不够用——那这篇就是为你写的。它不讲抽象理论只讲你在.ld文件里敲下的每一行对应着芯片手册哪一页的哪个寄存器、哪个时序图。2. 内容整体设计与思路拆解从芯片手册到.ld文件的完整映射链2.1 为什么不能直接用IDE自动生成的ld脚本很多新手会说“Keil/IAR不是自带startup和linker script吗我改个芯片型号就行。” 这话对了一半但恰恰是这一半害人最深。IDE自动生成的脚本本质是通用模板它假设你用的是标准外设库Standard Peripheral Library或HAL库且默认启用所有可能用到的内存区域比如把整个64KB Flash都划给代码20KB SRAM全当RAM用。但现实项目远比模板复杂你可能用STM32F103C8T6国产替代芯片如GD32F103C8T6其Flash擦除粒度是2KB而非1KB若脚本里.text段末尾没对齐OTA升级时整块擦除会误删关键数据你可能要做USB HID设备但STM32F103C8T6没有原生USB差分信号引脚得用软件模拟这就需要预留一大块SRAM做USB协议栈的缓冲区而默认脚本把SRAM全分给.data/.bss结果USB收发直接崩溃你甚至可能用PA8脚PWMDMA驱动WS2812灯带DMA传输需要连续的、非cacheable的内存区域而默认脚本没做内存属性标记导致DMA读取到脏数据。自动生成脚本的致命缺陷在于它把MCU当成一个黑盒而真实开发必须把它当作一张摊开的电路板——每个字节的地址、每个区域的访问时序、每个总线的带宽限制都得亲手标定。我在做智能温度监控系统时客户要求固件支持双备份主程序备份程序以便远程升级失败后回滚。这需要把Flash严格划分为0x08000000-0x0800FFFF主程序64KB、0x08010000-0x0801FFFF备份程序64KB、0x08020000-0x080203FF参数区1KB。自动生成脚本只会给你一个64KB的.text段根本无法满足这种精细分区需求。所以我们必须抛弃“拿来主义”从芯片数据手册Datasheet和参考手册Reference Manual出发构建一条不可绕过的映射链芯片物理资源 → 总线地址空间 → 链接器内存视图 → ld脚本语法描述 → 最终二进制布局。2.2 映射链第一步读懂STM32F103C8T6的内存拓扑STM32F103C8T6的内存不是一块平铺直叙的大饼而是由多条总线AHB, APB连接的多个独立模块每块都有自己的地址窗口和访问特性。核心三块是Flash存储器起始地址0x08000000容量64KB0x00000000 - 0x0000FFFF。注意这是代码执行地址Execution AddressCPU从这里取指令。但Flash写入编程/擦除必须通过专用的Flash接口FPEC走的是APB2总线地址映射在0x40022000附近。这就是为什么网络热词里问“MCU内部的Flash是用什么接口访问的”——答案是读取用AHB总线直接映射写入用APB2总线寄存器控制。ld脚本只关心前者执行地址后者由驱动代码处理。SRAM起始地址0x20000000容量20KB0x00000000 - 0x00004FFF。这是数据运行地址Runtime Address全局变量、堆栈、DMA缓冲区都落在此处。但SRAM访问速度受AHB总线频率影响若系统时钟超72MHzF103最高需配置等待周期FLASH_ACR寄存器否则读写出错。ld脚本需确保.data段加载到Flash后能正确复制到此地址。位带区Bit-Band这不是独立内存而是对SRAM和外设寄存器的“位操作映射”。例如SRAM的位带区从0x22000000开始将0x20000000处的每个bit映射为一个32-bit字。ld脚本通常不显式声明位带区因为它是硬件透明的但理解它能帮你避开陷阱——比如若你把.stack段起始地址设为0x22000000实际占用的是位带区映射的物理SRAM可能导致其他变量被覆盖。提示STM32F103C8T6原理图里VDDA/VSSA模拟电源必须独立滤波否则ADC采样值跳变。这虽与ld脚本无关但常被忽略。当你的.data段里有ADC校准系数数组且初始化失败时先查电源再查脚本。2.3 映射链第二步链接器视角的内存模型链接器ld不关心物理总线它只认一个抽象概念MEMORY区域。你需要用MEMORY命令把芯片的物理地址空间定义成几个逻辑区域并赋予名称如FLASH,RAM。每个区域有ORIGIN起始地址和LENGTH长度。例如MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K }这里(rx)表示该区域可读r可执行x(rwx)表示可读r可写w可执行x——注意SRAM标x是因为某些场景如动态加载代码需要但通常不启用。关键点在于LENGTH必须严格等于芯片手册标注的容量不能四舍五入STM32F103C8T6的SRAM是20KB即0x5000字节20 * 1024 20480 0x5000若写成LENGTH 20K链接器会按1024进制算20102420480没问题但若写成LENGTH 20480也正确可一旦写成LENGTH 20000十进制就少了480字节链接器会在分配.stack时越界导致栈溢出无声崩溃。我在做智能门锁项目时曾因把LENGTH 20K误写为LENGTH 20k小写kGCC链接器将其解释为20100020000字节结果.stack顶点撞上了.bss起点门锁在连续输入错误密码10次后死机——因为计数器变量被栈破坏。2.4 映射链第三步段Section与区域的绑定逻辑定义好MEMORY区域后下一步是用SECTIONS命令把编译器生成的各种段.text,.rodata,.data,.bss,.stack等分配到具体区域并指定它们的顺序和对齐。这步的核心逻辑是分离加载地址LMA与运行地址VMALMALoad Memory Address程序烧录Flash时这段数据存放的物理地址。例如.text段的LMA就是Flash里的地址。VMAVirtual Memory Address程序运行时这段数据实际被CPU访问的地址。例如.data段的LMA在Flash里只读但VMA在SRAM里可读写启动时需从LMA复制到VMA。为什么必须分离因为.data段包含已初始化的全局变量如int flag 1;这些值在编译时就确定了必须固化在Flash里但运行时变量要被修改必须放在可写的SRAM里。链接器通过AT FLASH语法指定LMA通过 RAM指定VMA。一个典型SECTIONS骨架如下SECTIONS { .text : { *(.vectors) /* 中断向量表必须在Flash最开头 */ *(.text) /* 代码 */ *(.rodata) /* 只读数据如字符串常量 */ } FLASH .data : AT(ADDR(.text) SIZEOF(.text)) { /* LMA紧接.text之后 */ *(.data) /* 已初始化数据 */ . ALIGN(4); /* 4字节对齐ARM指令要求 */ _sidata .; /* 记录.data在Flash中的起始地址供启动代码复制用 */ } RAM /* VMA在RAM里 */ .bss : { *(.bss) *(COMMON) . ALIGN(4); _sbss .; /* .bss起始地址 */ _ebss .; /* .bss结束地址用于清零 */ } RAM }这里.vectors段必须放在.text最前面因为STM32复位后CPU从0x08000000读取MSP主堆栈指针值从0x08000004读取复位向量地址。若向量表不在0x08000000芯片根本不会启动。而_sidata,_sbss等符号是给C启动文件如startup_stm32f103xb.s用的它在main()前执行负责把.data从Flash复制到SRAM并把.bss清零。ld脚本不是孤立存在它和启动代码是孪生兄弟缺一不可。忘记定义_sidata你的全局变量永远是0不管你在代码里赋了什么初值。3. 核心细节解析与实操要点逐行拆解一个生产级ld脚本3.1 完整脚本示例与关键注释以下是一个专为STM32F103C8T6最小系统板优化的ld脚本stm32f103c8t6_custom.ld已用于智能温度监控系统和门锁项目经量产验证。我们逐段解析/* STM32F103C8T6 Custom Linker Script Target: arm-none-eabi-gcc (GNU Arm Embedded Toolchain) Author: Embedded Systems Practitioner Date: 2024-06 */ /* 1. MEMORY区域定义严格匹配芯片手册 */ MEMORY { /* Flash: 64KB, from 0x08000000 to 0x0800FFFF */ FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K /* SRAM: 20KB, from 0x20000000 to 0x20004FFF */ RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K /* Optional: Define a dedicated region for USB buffer (if using SW USB) */ /* USB_RAM (rwx) : ORIGIN 0x20005000, LENGTH 2K */ } /* 2. ENTRY point: 指定复位处理函数入口 */ ENTRY(Reset_Handler) /* 3. SECTIONS: 内存布局核心 */ SECTIONS { /* 3.1 向量表必须位于Flash起始 */ .vectors : { . ALIGN(4); __vector_table_start__ .; KEEP(*(.vectors)) . ALIGN(4); __vector_table_end__ .; } FLASH /* 3.2 代码段包含向量表后的所有可执行代码 */ .text : { . ALIGN(4); *(.text) *(.text.*) *(.rodata) *(.rodata.*) . ALIGN(4); _etext .; /* End of text section */ } FLASH /* 3.3 已初始化数据段LMA在FlashVMA在RAM */ .data : AT(ADDR(.text) SIZEOF(.text)) /* LMA: 紧接.text之后 */ { . ALIGN(4); _sdata .; /* Start of data section in RAM */ *(.data) *(.data.*) . ALIGN(4); _edata .; /* End of data section in RAM */ } RAM /* VMA: 在RAM中 */ /* 3.4 未初始化数据段仅需VMALMA无意义全0 */ .bss : { . ALIGN(4); _sbss .; /* Start of bss section */ *(.bss) *(COMMON) . ALIGN(4); _ebss .; /* End of bss section */ } RAM /* 3.5 堆Heap用于malloc/free从.bss后开始向上增长 */ .heap (NOLOAD) : { . ALIGN(8); _sheap .; . 2K; /* Reserve 2KB heap space */ . ALIGN(8); _eheap .; } RAM /* 3.6 栈Stack用于函数调用从RAM末尾开始向下增长 */ .stack (NOLOAD) : { . ALIGN(8); _estack .; /* Stack top (highest address) */ . 2K; /* Reserve 2KB stack space */ . ALIGN(8); _sstack .; /* Stack bottom (lowest address) */ } RAM /* 3.7 填充区确保Flash末尾对齐便于OTA升级 */ .flash_padding : { . ALIGN(1024); /* Align to 1KB boundary */ BYTE(0xFF) /* Fill with 0xFF (erased state) */ } FLASH }3.2 关键细节深度解读3.2.1ENTRY(Reset_Handler)的隐含契约ENTRY指定链接器的入口符号它必须与启动文件startup_stm32f103xb.s中的Reset_Handler标签完全一致。如果启动文件里写的是Reset_Handler而你脚本里写ENTRY(_start)链接器会报错undefined reference to _start。更隐蔽的坑是某些国产替代芯片如GD32F103C8T6的启动文件可能用Reset_Handler但部分旧版HAL库用Default_Handler。必须确保启动文件、ld脚本、CMSIS头文件三者入口符号统一。我在做STM32F103C8T6国产替代项目时因GD32的启动文件用Reset_Handler而移植的ST HAL库例程用Default_Handler导致链接成功但复位后跳到非法地址——因为Default_Handler在向量表里是空的。3.2.2.vectors段的绝对位置要求.vectors段必须是.text的第一个子段且__vector_table_start__必须等于0x08000000。脚本里用KEEP(*(.vectors))确保即使该段未被引用也不会被链接器丢弃。ALIGN(4)保证向量表4字节对齐ARM Thumb指令要求。若你用FreeRTOS其port.c里会定义vPortSVCHandler等异常处理函数它们的地址会被链接到向量表相应偏移。向量表大小固定为256字64个32-bit向量若你添加了自定义中断如EXTI9_5_IRQHandler必须确保它在向量表中有对应槽位否则中断永不触发。STM32F103C8T6的EXTI9_5位于向量表偏移0x58第22个向量若你的.vectors段没包含它中断服务函数再完美也白搭。3.2.3.data段的AT()语法精要AT(ADDR(.text) SIZEOF(.text))是ld脚本最易错的语法之一。ADDR(.text)返回.text段的VMA即0x08000000 向量表大小SIZEOF(.text)返回其长度字节数。两者相加得到.data在Flash中的起始地址。必须用AT()不能用 FLASH否则.data的LMA和VMA都指向RAM导致烧录时数据丢失。实测案例某次我误将.data的AT(...)删掉只留 RAM编译后hex文件里.data内容全为0烧录后所有全局变量初值都是0调试半天才发现是脚本问题。3.2.4.heap和.stack的NOLOAD属性NOLOAD告诉链接器这部分内存不包含在最终的二进制镜像.bin/.hex里只在运行时由启动代码分配。.heap从.bss结束处_ebss开始向上增长.stack从RAM末尾0x20004FFF开始向下增长。二者之间必须留有安全间隙否则栈溢出会覆盖堆或堆分配会撞上栈底。脚本里各预留2KB是基于经验FreeRTOS任务栈默认256字主任务栈512字加上中断嵌套2KB足够。切记.stack的_estack是栈顶最高地址_sstack是栈底最低地址C启动代码用_estack初始化MSP寄存器。若你把_estack设错了比如设成0x20000000RAM起始栈就会向下生长到非法地址立即硬故障。3.2.5.flash_padding的OTA友好设计BYTE(0xFF)填充确保Flash末尾是擦除态0xFF这对OTA升级至关重要。STM32F103C8T6的Flash以1KB为单位擦除若你的固件大小是32.5KB未填充的0.5KB在升级时可能被新固件覆盖导致擦除不完整。ALIGN(1024)强制对齐到1KB边界BYTE(0xFF)填满剩余空间。这是量产项目的必备项绝非可有可无。我们在智能门锁项目中因早期版本没加padding一次OTA升级后门锁无法启动返工率高达15%。3.3 实操心得那些文档里不会写的坑坑1.rodata段的陷阱字符串常量如printf(Hello)默认放在.rodata段。若你用printf但没重定向fputc它会尝试写UART但UART未初始化导致死循环。更隐蔽的是.rodata若没显式放入.text段链接器可能把它放到.text之后、.data之前导致Flash布局碎片化。解决方案在.text段内显式包含*(.rodata)并确保ALIGN(4)。坑2__attribute__((section(.my_section)))的兼容性你想把关键算法放特定Flash区域如加密密钥用__attribute__((section(.key)))定义变量。但若ld脚本没声明.key段链接器会报错。必须在SECTIONS里添加.key (NOLOAD) : { *(.key) } FLASHNOLOAD防止它被复制到RAM密钥不该在RAM明文存在。坑3-Wl,--defxxx.def与ld脚本冲突Keil/IAR有时用.def文件导出符号若同时用ld脚本可能符号重复定义。原则选一种方式不要混用。GCC项目一律用ld脚本Keil项目用其内置scatter文件。坑4LENGTH 20KvsLENGTH 20480表面看一样但20K是链接器内置单位1024进制20480是十进制。若你用LENGTH 20000十进制链接器不会警告但会少480字节。永远用64K/20K或精确计算十六进制0x10000/0x5000。4. 实操过程与核心环节实现从零构建一个可烧录的工程4.1 环境准备工具链与最小依赖我们使用开源工具链避免Keil/IAR授权限制也更贴近底层编译器arm-none-eabi-gccGNU Arm Embedded Toolchain 10.3或更新链接器arm-none-eabi-ld同上工具链调试器openocdgdb支持ST-Link/V2构建系统Makefile手动控制杜绝IDE黑盒所需文件清单main.c最简应用LED闪烁startup_stm32f103xb.sCMSIS标准启动文件从STM32CubeMX或官方库获取system_stm32f10x.c系统时钟初始化SystemInit()stm32f103c8t6_custom.ld本文脚本Makefile编译规则注意STM32F103C8T6数据手册明确指出其Flash编程电压为2.0V-3.6V若VDD低于2.0V写入会失败。调试时务必用电压表确认VDD稳定在3.3V±5%。4.2 Step-by-Step手写第一个ld脚本并验证步骤1创建基础脚本框架新建文件stm32f103c8t6_custom.ld粘贴MEMORY和ENTRY部分。此时编译会失败因为缺少SECTIONS。步骤2添加.vectors和.text段加入.vectors段确保KEEP(*(.vectors))存在。编译main.c含空main()用arm-none-eabi-objdump -h build/main.o查看目标文件段信息确认.vectors段被生成。步骤3添加.data和.bss段加入.data段注意AT()语法。此时编译仍会失败因为启动文件需要_sidata,_sdata,_edata等符号。打开startup_stm32f103xb.s搜索_sidata确认它被用于CopyDataInit函数。步骤4添加.heap和.stack段这是最容易出错的步骤。在startup_stm32f103xb.s里找到Stack_Size定义通常为0x400将其改为_estack - _sstack。我的脚本里预留2KB所以Stack_Size应为0x800。若你改了.stack大小必须同步修改启动文件里的Stack_Size否则栈大小不匹配。步骤5编译并检查布局运行make生成main.elf。用以下命令检查# 查看各段地址和大小 arm-none-eabi-objdump -h build/main.elf # 查看符号表确认_sdata, _edata等存在 arm-none-eabi-nm -n build/main.elf | grep -E (sdata|edata|sbss|ebss|estack) # 生成map文件看详细布局 arm-none-eabi-gcc -T stm32f103c8t6_custom.ld ... -Wl,-Mapbuild/main.map关键检查点.vectorsVMA 0x08000000.textVMA 0x08000000 向量表大小通常0x100.dataLMA .text结束地址VMA 0x20000000 偏移_estack0x20004FFFRAM末尾步骤6烧录并硬件验证用openocd烧录main.binopenocd -f interface/stlink.cfg -f target/stm32f10x.cfg -c program build/main.bin verify reset exit接上LED观察是否闪烁。若不闪用gdb连接arm-none-eabi-gdb build/main.elf (gdb) target extended-remote :3333 (gdb) monitor reset halt (gdb) info registers # 查看PC是否在Reset_Handler (gdb) stepi # 单步执行看是否进入main4.3 场景延展适配不同需求的脚本改造场景1FreeRTOS移植堆空间扩展FreeRTOS需要大量堆空间管理任务。修改.heap段.heap (NOLOAD) : { . ALIGN(8); _sheap .; . 8K; /* 从2KB增至8KB */ . ALIGN(8); _eheap .; } RAM并在FreeRTOSConfig.h中设置#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 8 * 1024 ) ) #define configAPPLICATION_ALLOCATED_HEAP 1 /* 告诉FreeRTOS用外部堆 */注意configAPPLICATION_ALLOCATED_HEAP1必须开启否则FreeRTOS会用自己的pvPortMalloc与ld脚本冲突。场景2USB HID软件模拟专用RAM区STM32F103C8T6无USB引脚需用PA11/PA12模拟。USB协议栈需2KB DMA缓冲区。在MEMORY中添加USB_RAM (rwx) : ORIGIN 0x20005000, LENGTH 2K在SECTIONS中添加.usb_buffer (NOLOAD) : { . ALIGN(4); _susb_buf .; . 2K; . ALIGN(4); _eusb_buf .; } USB_RAM在C代码中uint8_t __attribute__((section(.usb_buffer))) usb_rx_buffer[1024]; uint8_t __attribute__((section(.usb_buffer))) usb_tx_buffer[1024];场景3OTA双区升级Flash分区将Flash划为两区MEMORY { FLASH_APP1 (rx) : ORIGIN 0x08000000, LENGTH 32K FLASH_APP2 (rx) : ORIGIN 0x08008000, LENGTH 32K FLASH_PARAM (rx) : ORIGIN 0x08010000, LENGTH 1K }然后为每个区写独立ld脚本app1.ld,app2.ld编译时指定-T app1.ld。关键FLASH_PARAM区必须用AT()指定LMA因为参数需在运行时读写但烧录时固化在Flash。4.4 参数计算实录为什么是64K和20KFlash容量64KSTM32F103C8T6数据手册DS5319第2页明确标注“64 Kbytes of Flash memory”。64K 64 * 1024 65536 0x10000字节。起始地址0x08000000结束地址 0x08000000 0x10000 - 1 0x0800FFFF。SRAM容量20K手册第2页“20 Kbytes of general-purpose SRAM”。20K 20 * 1024 20480 0x5000字节。起始地址0x20000000结束地址 0x20000000 0x5000 - 1 0x20004FFF。向量表大小256字Cortex-M3/M4内核规范规定复位向量表固定256字64个32-bit入口。每个向量4字节故占256字节。STM32F103C8T6的向量表从0x08000000开始占0x00000000-0x000000FF。栈大小2K经验公式栈大小 (最大中断嵌套深度 * 32) (最大函数调用深度 * 64) 256。F103C8T6无FPU单次中断约20字寄存器压栈主循环函数调用深度5故2K0x800绰绰有余。5. 常见问题与排查技巧实录从现象到根源的速查表5.1 典型问题速查表现象可能原因排查命令解决方案烧录后LED不亮调试器连不上.vectors未在0x08000000或ENTRY不匹配arm-none-eabi-objdump -h main.elf | grep vectors检查.vectors段VMA是否为0x08000000确认ENTRY与启动文件标签一致**串口有输出但内容乱码
返回列表