启动文件+链接文件

张开发
2026/4/7 4:20:45 15 分钟阅读

分享文章

启动文件+链接文件
启动文件startup_xxx.s核心作用启动文件是汇编编写的底层代码是芯片上电后执行的第一部分程序核心职责围绕“系统初始化”和“数据准备”其中就包含「数据搬运」和「BSS清零」操作关键要点如下架构与指令配置指定CPU内核如Cortex-M3、指令集Thumb、语法规范确保汇编代码能被正确编译执行全局符号暴露声明中断向量表g_pfnVectors、默认异常处理函数Default_Handler等供内核和C代码引用核心启动流程复位入口Reset_Handler是核心内部包含.data段搬运LoopCopyDataInit循环、.bss段清零LoopFillZerobss循环完成后调用SystemInit和main函数进入C语言世界依赖链接脚本启动文件中用到的地址符号如_sidata、_sdata等均由链接脚本定义自身不负责地址分配。链接脚本xxx.ld核心作用链接脚本是“内存规划师”负责划分Flash和RAM的内存空间、定义各段.text、.data、.bss等的地址范围同时导出关键地址符号供启动文件使用关键要点如下内存划分MEMORY定义Flash只读存放代码、常量和RAM可读写存放变量、栈堆的起始地址和大小比如STM32常见的Flash起始地址0x08000000、RAM起始地址0x20000000段定义SECTIONS将程序中的代码、数据分配到指定内存区域重点是定义.data和.bss段的地址范围并导出关键符号关键符号导出为启动文件提供.data和.bss段的地址标记包括_sidata.data在Flash的加载地址、_sdata.data在RAM的起始地址、_edata.data在RAM的结束地址、_sbss.bss在RAM的起始地址、_ebss.bss在RAM的结束地址这是数据搬运的核心依据栈堆配置定义栈顶地址_estack、堆的地址范围为程序运行提供动态内存支持。简单来说链接脚本“规划地址”启动文件“执行搬运”二者协同配合才能完成系统启动的核心准备工作。理解这一点再看“数据搬运”内容会更清晰易懂。在ARM Cortex-M系列MCUSTM32等 RTOS/裸机开发中我们常说“启动汇编要把数据从Flash搬到RAM”但很多新手会困惑到底哪些内容需要搬为什么要搬不搬会怎么样结合之前解析的启动汇编startup_xxx.s和链接脚本xxx.ld这篇博客专门拆解「启动时需搬运到RAM的内容」搭配代码示例、原理说明彻底讲透底层逻辑新手也能轻松理解。先明确核心前提Flash是只读存储器断电不丢数据RAM是随机存取存储器断电丢数据、可读写。程序运行时需要读写的变量必须放在RAM而固化在Flash里的代码、只读数据无需搬运直接执行/读取即可。一、核心搬运内容.data 段带初始化值的可读写数据这是启动时唯一必须从Flash搬运到RAM的内容也是启动汇编中「LoopCopyDataInit」循环的核心作用对象。哪些数据属于.data段必搬所有带明确初始化值的全局变量、静态变量都会被编译器分配到.data段必须从Flash搬运到RAM才能正常读写。代码示例直观区分// 必须搬运到RAM.data段intg_global_var100;// 全局变量 初始化值staticintg_static_var0x55;// 静态全局变量 初始化值typedefstruct{inta;charb;}MyStruct;MyStruct g_struct{1,a};// 全局结构体 初始化值intg_arr[5]{1,2,3};// 全局数组 部分初始化值// 不搬运不属于.data段intg_empty_var;// 全局变量 无初始化值进.bss段staticintg_empty_static;// 静态变量 无初始化值进.bss段constcharg_str[]hello;// 只读常量进.rodata段留Flash为什么必须搬运Flash是只读的而.data段的变量是「可读写」的——比如我们在代码中修改g_global_var的值g_global_var 200;如果变量还在Flash会因为只读属性报错甚至触发硬件异常HardFault。搬运原理结合之前的启动汇编链接脚本烧录时.data段的「初始化值」会被存储在Flash中由链接脚本定义的「_sidata」符号标记起始地址启动时启动汇编的「CopyDataInit」循环会把Flash中「_sidata」地址的数据逐一拷贝到RAM中「_sdata ~ _edata」的地址区间.data段运行地址运行时CPU直接操作RAM中的.data段变量实现读写功能。特殊情况个别编译器的自定义配置大部分场景下.data段的搬运逻辑是固定的但有两种特殊情况需注意大体积初始化常量如果定义了超大的初始化数组比如const int g_big_arr[1024] {1,2,…};个别编译器会将其分配到.data段而非.rodata此时也需要搬运极少场景只读初始化变量如果给变量加了const修饰且编译器优化后放入.rodata段则无需搬运直接留在Flash只读即可。二、无需搬运但需初始化.bss 段无初始化可读写数据.bss段的内容不需要从Flash搬运但启动时必须在RAM中做「清零」操作——这也是启动汇编中「LoopFillZerobss」循环的作用。哪些数据属于.bss段不搬只清零所有无初始化或初始化值为0的全局变量、静态变量都会被分配到.bss段直接在RAM中预留地址启动时清零即可。代码示例// 不搬运仅在RAM中清零.bss段intg_empty;// 无初始化值默认清0staticcharg_buf[1024];// 无初始化的静态数组默认清0MyStruct g_empty_struct;// 无初始化的结构体默认清0intg_zero_var0;// 初始化值为0也会进.bss段编译器优化为什么不需要搬运因为这些变量没有明确的初始化值或初始化值为0不需要在Flash中存储任何数据——启动时直接将RAM中「_sbss ~ _ebss」的地址区间全部写0就能满足程序运行需求避免变量出现随机值导致玄学Bug。三、永远留在Flash不搬运的内容除了.data段其他大部分内容都不需要搬运直接留在Flash中即可既节省RAM空间也符合硬件特性。.text段程序的所有代码、函数指令比如main()、SystemInit()、中断处理函数CPU直接从Flash读取执行无需搬运.rodata段只读常量比如字符串常量、const修饰的只读变量只能读取不能修改留在Flash即可中断向量表.isr_vector芯片上电后首先读取Flash首地址的向量表无需搬运到RAM固件相关信息比如固件版本号、校验码、OTA升级标记等只读不写留在Flash。四、补充栈Stack和堆Heap的特殊说明栈和堆都在RAM中但既不属于.data段也不属于.bss段无需从Flash搬运启动时仅做地址预留栈.stack作用存放函数局部变量、函数调用栈帧、中断现场保护启动逻辑链接脚本定义栈顶地址_estack/__StackTop启动时仅初始化栈指针不搬运、不清零后续运行时由函数调用自动覆盖。堆.heap作用动态内存分配比如malloc()、RTOS的内存池启动逻辑链接脚本划定堆的地址区间通常从_end符号开始启动时不做任何操作运行时由程序/RTOS自行管理分配、释放。五、结合启动流程的闭环思考回顾之前的启动汇编其实整个搬运/初始化流程就两件事搬运.data段把Flash中固化的初始化值搬到RAM的指定地址让可读写变量能正常修改清零.bss段把RAM中预留的无初始化变量地址全部清0避免随机值导致程序异常。这两步完成后CPU才能正常调用SystemInit()、进入main()跑裸机/RTOS业务逻辑——如果搬运失败比如链接脚本地址定义错误会直接导致程序跑飞、全局变量值异常、HardFault等问题。六、常见踩坑提醒坑1修改链接脚本后忘记同步.data/.bss段地址导致搬运时地址越界程序跑飞坑2把可读写变量误加const修饰放入.rodata段导致运行时修改报错坑3忽略.bss段清零未初始化全局变量出现随机值偶现玄学Bug坑4栈/堆地址预留不足导致函数调用栈溢出、动态内存分配失败。文末总结启动时的“Flash→RAM”搬运核心只针对「.data段」——带初始化值的可读写变量这是由Flash只读、RAM可读写的硬件特性决定的。理解清楚哪些内容需要搬运、哪些不需要既能帮我们排查启动异常问题也能在优化内存时比如节省RAM合理规划变量的定义方式比如尽量用const修饰只读常量、减少不必要的初始化值。如果结合之前的启动汇编和链接脚本就能彻底打通“内存划分→地址定义→数据搬运→程序启动”的完整底层逻辑

更多文章