来吧,一篇搞懂嵌入式链接文件!

张开发
2026/4/11 8:07:35 15 分钟阅读

分享文章

来吧,一篇搞懂嵌入式链接文件!
做嵌入式开发的朋友大概率都遇到过这样的场景编译完一个工程输出目录里一堆文件.axf、.elf、.bin、.hex、.sct、.ld…… 后缀五花八门看着就头大。很多人只知道 “这个是用来烧录的那个是用来调试的”但到底它们是怎么来的彼此之间有什么关系为什么 Keil 和 GCC 生成的文件后缀还不一样今天我们就从嵌入式软件的完整编译流程出发把这些文件的来龙去脉一次性讲清楚看完你再也不会搞混这些后缀了。先看总览从代码到运行的完整流程不管你用的是 Keil MDK还是 GCC ARM 工具链整个从写代码到芯片运行的逻辑完全一致区别只是不同工具链对产物的命名不一样。接下来我们就顺着这个流程一步步拆解每个阶段的文件到底是什么。第一步编译阶段生成.o目标文件一切的起点是我们自己写的代码.c源文件和.h头文件这部分大家都很熟悉就不多说了。编译器比如 Keil 的armcc或者 GCC 的arm-none-eabi-gcc会把每个.c文件单独进行编译把 C 代码转换成处理器能看懂的机器码最终生成一个对应的.o文件也就是目标文件Object File。这里要注意这个阶段的编译是 “单文件” 的每个.c文件只关心自己的代码不管其他文件的函数、变量在哪里。所以生成的.o文件里所有的地址都是相对地址函数调用、变量访问都还没有绑定到最终的绝对内存地址。简单来说.o文件就是一个 “半成品”它只包含了当前这个源文件的机器码还需要下一步的链接操作把所有的半成品拼起来分配最终的地址。第二步链接的 “导航图”——.sctvs.ld链接脚本有了一堆.o半成品接下来就要进入链接阶段了。链接器要做的事情就是把所有的.o文件、还有用到的库文件整合到一起给所有的函数、变量分配最终的绝对地址把零散的模块拼成一个完整的程序。但问题来了链接器怎么知道哪些代码要放到 Flash 里哪些数据要放到 RAM 里Flash 的起始地址是多少RAM 有多大有些要搬到 RAM 里运行的代码要怎么处理这就是链接脚本的作用了它就是给链接器看的 “导航图”告诉链接器整个芯片的内存布局以及各个代码段、数据段要放到哪个地址。而这里Keil 和 GCC 就出现了第一个命名差异Keil MDK 用的是.sct文件全称是 Scatter-Loading Description File也就是分散加载描述文件。GCC ARM 用的是.ld文件全称是 Linker Script也就是链接器脚本。它们的核心功能完全一致都是用来定义内存布局、指导链接器工作的只是语法和表述方式不一样.sct文件用Load Region加载域也就是数据烧录到 Flash 的地址和Execution Region执行域也就是程序运行时的地址的概念把加载地址和运行地址明确分离开比如你要把一部分代码从 Flash 加载到 RAM 里运行在 sct 里可以很清晰地定义出来。.ld文件的语法更偏向声明式类似 C 语言的风格先定义MEMORY区域比如 Flash 和 RAM 的起始地址和大小然后在SECTIONS里把各个段.text代码段、.data数据段等分配到对应的内存区域里。举个最简单的例子STM32F103 的默认链接配置在 sct 里你会看到类似这样的定义LR_IROM1 0x08000000 0x00080000 { ; 加载域Flash的起始地址和大小 ER_IROM1 0x08000000 0x00080000 { ; 执行域代码运行在Flash *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00010000 { ; 数据运行在RAM .ANY (RW ZI) } }而在 ld 文件里对应的定义是这样的MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 512K RAM (xrw) : ORIGIN 0x20000000, LENGTH 64K } SECTIONS { .text : { *(.text) /* 其他代码段 */ } FLASH .data : { *(.data) /* 其他数据段 */ } RAM AT FLASH }不管语法怎么变核心都是一件事告诉链接器内存怎么分代码放哪里数据放哪里。没有这个文件链接器根本不知道怎么把零散的.o文件拼成一个能在芯片上运行的程序。第三步完整版可执行文件 ——.axfvs.elf有了链接脚本的指导链接器就可以开始工作了把所有的.o文件整合起来分配好地址最终生成一个完整的可执行文件。这里Keil 和 GCC 又出现了第二个命名差异Keil MDK 生成的是.axf文件全称是 ARM eXecutable FormatARM 可执行格式。GCC ARM 生成的是.elf文件全称是 Executable and Linkable Format可执行链接格式。很多人会问这两个有什么区别其实答案很简单.axf本质上就是.elf格式的 ARM 扩展它们的底层都是标准的 ELF 格式只是.axf在标准 ELF 的基础上额外增加了一些 ARM 特有的调试信息、重定位信息用来适配 Keil 的调试工具链。这两个文件有个共同的特点它们非常 “完整”甚至有点 “臃肿”。它们里面除了真正要运行的机器码代码段、初始化数据段之外还包含了完整的符号表所有函数、变量的名字和地址源码行号映射把机器码的地址对应到源代码的行号调试信息变量的类型、函数的调用关系等等开发阶段需要的信息所以这两个文件的体积通常都很大比如一个小的 STM32 工程最终的烧录固件可能只有 10KB但.axf或者.elf文件可能有几百 KB多出来的大部分都是调试信息。那它们的用途是什么它们是给调试用的比如你在 Keil 里点击 Debug下载到芯片里的就是这个.axf文件你用 J-Link 的 J-Scope 监控变量也需要加载这个文件。只有有了这些调试信息你才能在 IDE 里看到源代码、下断点、单步执行、查看变量的值 —— 如果没有这些信息你只能看到一堆二进制的机器码根本没法调试。但是这些调试信息只有开发阶段才有用量产烧录的时候我们根本不需要这些东西它们只会浪费 Flash 的空间。所以我们还需要下一步把这些多余的信息去掉生成精简的烧录固件。第四步烧录用的精简固件 ——.binvs.hex为了得到可以烧录到芯片里的精简固件我们会用工具Keil 的fromelf或者 GCC 的objcopy把.axf/.elf里的调试信息、符号表这些没用的东西全部去掉只保留真正要烧录到 Flash 里的机器码和初始化数据。最终生成的就是我们最常用的两种烧录文件.bin和.hex。这两个文件的区别很多人一直搞不清其实一句话就能说清楚一个是纯二进制一个是带地址信息的文本格式。.bin纯二进制固件.bin是最纯粹的二进制文件它把所有要烧录的有效数据按地址从小到大的顺序直接排列起来没有任何额外的信息。它的优点很明显体积最小没有任何多余的开销10KB 的固件就是 10KB 的文件一点都不浪费。但是它有个很大的限制它默认你的固件的所有地址是连续的。举个例子如果你的固件所有的代码和数据都是从0x08000000开始连续的 10KB 空间那 bin 文件完全没问题烧录器把这 10KB 的数据从0x08000000开始写进去就行。但如果你的固件有非连续的地址呢比如你的 Bootloader 在0x08000000App 在0x08008000还有一部分配置数据在0x08010000中间空了很多区域。或者你用了分散加载把一部分数据放到了 Flash 的其他位置。这时候 bin 文件就处理不了了因为它是连续的它会把从最低地址到最高地址之间的所有空间不管你有没有用到都打包进去导致文件变得巨大而且烧录的时候还会把中间空的区域也擦写这显然不是我们想要的。.hex带地址的通用固件.hex全称是 Intel HEX 文件是一种文本格式的固件文件。它的每一行都是一条记录里面包含了这部分数据的起始地址、数据长度、具体的数据还有校验和。比如你打开一个 hex 文件会看到类似这样的内容每一行开头的:是标记然后是长度、地址、类型、数据、校验和。这种格式的好处是什么它可以处理非连续的地址比如刚才的例子它可以先写0x08000000开始的 Bootloader然后跳过中间的空区域再写0x08008000开始的 App然后再写0x08010000开始的配置数据。中间的空区域不需要管也不会占用文件的空间。而且它自带校验和烧录器可以自动校验每一行的数据有没有出错可靠性更高。当然它也有缺点因为是文本格式每个字节的二进制要转成两个十六进制字符还要加上地址、校验这些额外的信息所以它的体积会比 bin 文件大一点通常会大 30% 左右但对于现在的存储来说这点差距完全可以忽略。所以总结一下如果你的固件地址是连续的用 bin 没问题体积小一点。如果你有非连续的地址或者你不确定直接用 hex 就对了兼容性更好这也是为什么大部分开发工具默认生成 hex 文件的原因。最后烧录与运行拿到了 bin 或者 hex 文件我们就可以把它烧录到芯片的 Flash 里了。等芯片上电之后它会从固定的入口地址比如 STM32 的0x08000000开始取指令我们的程序就正式跑起来了。到这里整个从代码到运行的流程就走完了所有的文件我们也都拆解清楚了。一张表总结所有文件最后我们把所有的文件整理成一文件后缀对应工具链所属阶段核心作用.c/.h通用源码阶段开发者编写的源代码.o通用编译阶段单个源文件编译后的可重定位目标文件.sctKeil MDK链接配置Keil 的分散加载描述文件定义内存布局.ldGCC ARM链接配置GCC 的链接器脚本定义内存布局.axfKeil MDK链接输出Keil 的完整可执行文件含调试信息用于调试.elfGCC ARM链接输出GCC 的标准可执行文件含调试信息用于调试.bin通用固件输出纯二进制精简固件用于连续地址的烧录.hex通用固件输出Intel HEX 格式固件支持非连续地址通用烧录格式常见疑问解答1. 为什么调试用 axf/elf烧录用 bin/hex因为调试需要符号表、行号映射这些调试信息才能让你看到源码、下断点、查看变量这些信息只有 axf/elf 里有。而烧录只需要真正的机器码调试信息没用去掉之后体积更小烧录更快。2. 为什么 Keil 和 GCC 的文件后缀不一样这是历史原因Keil 用的是 ARM 的早期工具链所以用了自己的一套命名比如 axf、sct而 GCC 用的是 Unix 体系下的标准命名比如 elf、ld。但它们的核心逻辑是完全一样的只是工具不同所以后缀不同。3. 能不能把 axf/elf 直接烧录到芯片里理论上可以但是完全没必要因为里面有大量没用的调试信息会浪费很多 Flash 空间而且很多烧录器也不支持直接烧录 elf/axf 格式。看完这些是不是突然发现这些乱七八糟的后缀其实就是编译流程不同阶段的产物而已搞懂了整个流程不管是 Keil 还是 GCC不管是什么后缀你都能一眼看明白它是干嘛的再也不会搞混了。

更多文章