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

文章详情

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

嵌入式开发必知:HEX、BIN、ELF、SREC文件格式深度解析与转换实战

嵌入式开发必知:HEX、BIN、ELF、SREC文件格式深度解析与转换实战 1. 从一次烧录失败说起为什么我们需要理解文件格式转换那天下午我正忙着给一块新打样的STM32板子烧录程序。像往常一样我打开了STM32CubeIDE编译、链接一气呵成然后准备用ST-Link Utility把生成的.hex文件拖进去烧录。结果软件弹出一个错误“文件格式不支持”。我愣了一下检查了一下输出目录发现里面躺着一个.elf文件和一个.axf文件唯独没有我熟悉的.hex。这才想起来刚才手快在项目属性里把输出格式改成了“ELF/DWARF”忘了改回来。这个小小的失误让我重新审视了嵌入式开发中这些看似基础却至关重要的环节.hex、.bin、.elf、.srec……这些后缀名背后到底藏着什么秘密为什么Keil默认生成.hex而GCC工具链更偏爱.bin或.elf当我们需要在不同工具链、不同烧录器、甚至不同芯片厂商之间传递程序时如何让它们“说同一种语言”这不仅仅是点几下鼠标选择输出格式那么简单理解它们的本质差异和转换逻辑是解决无数诡异问题的钥匙比如为什么你的.bin文件烧进去芯片不跑或者为什么从.elf里提取不出想要的符号表。简单来说.hex和.srec是带有地址信息的文本格式.bin是纯二进制映像而.elf则是包含丰富调试信息的容器格式。它们各有各的战场.hex因其标准化和可读性是许多老牌烧录器和单片机的“官方语言”.bin因其极简和通用是量产烧录和系统升级的首选.elf则是我们开发调试时的“瑞士军刀”里面塞满了地址、符号、源码映射等信息.srec则可以看作是.hex的一个变种兄弟在摩托罗拉系和一些特定场景下仍有应用。接下来我们就抛开那些枯燥的文档从一个嵌入式开发者的实战视角把这些格式的里里外外、转换的门道和踩过的坑一次聊透。2. 庖丁解牛四大可执行文件格式的深度解析要玩转格式转换第一步必须是理解每个格式的“五脏六腑”。知其然更要知其所以然这样当转换出错时你才能一眼看出问题出在哪个环节。2.1 Intel HEX结构化的文本记录.hex文件全称Intel HEX是一种用ASCII文本字符来表示二进制数据的格式。它的设计非常巧妙把地址、数据、校验和都打包进了一行行可读的记录里。你完全可以用记事本打开一个.hex文件看到类似这样的内容:10010000214601360121470136007EFE09D2190140 :100110002146017E17C20001FF5F16002148011928 :00000001FF每一行都是一条独立的记录以冒号:开头。一条典型的HEX记录结构如下:[长度][地址][记录类型][数据][校验和]长度1字节表示后面[数据]字段的字节数。地址2字节表示这条记录中数据起始的负载地址Load Address。注意这是16位地址对于32位系统需要通过扩展地址记录类型0x04来指定高16位。记录类型1字节这是关键。常见的有00数据记录这是最常见的一种里面就是实际的程序代码或数据。01文件结束记录标志文件结尾。02扩展段地址记录用于指定后续数据记录的段地址实模式下使用。04扩展线性地址记录用于指定后续数据记录的高16位地址32位系统常用。比如:04000005080000F1表示后续数据的基地址是0x08000000STM32 Flash的典型起始地址。数据就是实际的二进制内容以十六进制ASCII码表示。校验和1字节计算方法是从[长度]到[数据]最后一个字节的所有值求和取结果的补码即0x100减去和值的低字节。接收方可以通过校验和验证该行数据在传输中是否出错。为什么Keil/IAR等IDE默认生成HEX因为它自带地址信息非常“懂事”。烧录器拿到.hex文件不需要你额外指定烧录地址它自己就能根据记录里的地址信息把数据写到Flash的正确位置。这对于包含多个非连续内存区域比如Flash、EEPROM的程序非常友好。此外文本格式便于查看、比对和简单的脚本处理在串口ISP等简单烧录方式中也很常用。2.2 Motorola S-RecordHEX的“表亲”.srec或.mot文件即Motorola S-Record格式是摩托罗拉公司定义的一种与Intel HEX类似但结构不同的文本格式。它长这样S315 08000000 08000200200100200901000805010008DF S30D 08000010 0901000805010008B2 S705 08000000 FA它的记录结构是S[类型][长度][地址][数据][校验和]。类型1位数字常见的有S0头部记录通常包含描述信息。S1、S2、S3数据记录分别对应2字节、3字节、4字节的地址字段。S34字节地址对应32位系统。S5记录计数可选。S7、S8、S9结束记录分别对应4字节、3字节、2字节的起始执行地址。长度1字节表示[地址][数据][校验和]的总字节数。地址2/3/4字节取决于记录类型。数据实际二进制内容。校验和1字节计算方式是0xFF - (从[长度]到[数据]末尾所有字节之和的低字节)。S-Record在早期的摩托罗拉微处理器如68K系列和某些PowerPC、ColdFire工具链中很常见。如今虽然Intel HEX更主流但在一些老旧的工业设备、特定的仿真器或工具链如某些版本的CodeWarrior中你依然可能会遇到它。从功能上讲它能完成HEX的所有任务只是语法不同。2.3 ELF功能强大的容器.elf文件Executable and Linkable Format是Linux/Unix系统和现代嵌入式工具链如GCC的标准输出格式。它远不止是代码和数据那么简单而是一个结构复杂的容器。一个ELF文件主要包含以下几部分ELF头描述了文件的基本信息如目标机器架构ARM、x86、入口地址、程序头表和节头表的位置等。程序头表告诉加载器如何将文件映射到进程的虚拟内存中。每个程序头描述一个段比如可加载的代码段.text、数据段.data、BSS段等包含该段在文件中的偏移、在内存中的虚拟地址、大小、访问权限R/W/X等。节头表告诉链接器和调试器文件的详细组织方式。每个节头描述一个节比如存放代码的.text节、存放只读数据的.rodata节、存放符号表的.symtab节、存放字符串表的.strtab节等。一个段可以由多个节组成。为什么调试离不开ELF因为ELF里存放了丰富的调试信息如果编译时加了-g选项。这些信息以DWARF或STABS格式存储在特定的节如.debug_info中建立了机器码与源代码行号、变量名、函数名、数据类型之间的映射关系。当你用GDB进行单步调试、查看变量时背后的功臣就是ELF文件。.axf文件本质上就是ARM编译器ARMCC或Arm Compiler 6生成的ELF文件只是换了个名字。ELF不能直接烧录大多数情况下是的。因为烧录器通常只关心纯粹的二进制指令和数据而不需要ELF里的符号表、调试信息、重定位信息等“元数据”。直接烧录ELF会导致烧录器无法解析这些额外信息而失败。因此我们需要从ELF中“提取”出纯净的二进制映像这就是生成.bin或.hex的过程。2.4 BIN纯粹的二进制映像.bin文件是最简单、最原始的可执行文件格式。它就是一段连续的二进制数据没有任何头部、地址、校验和等附加信息。你可以把它理解为内存或Flash某个区域的直接映像。BIN文件的优缺点优点体积最小只包含有效数据结构最简单几乎被所有底层烧录工具和Bootloader支持。在量产烧录、OTA升级时传输和存储效率最高。缺点它“不知道自己该去哪”。烧录.bin文件时你必须明确告诉烧录器目标地址。比如对于STM32你通常需要将.bin文件烧录到0x08000000这个起始地址。如果你选错了地址程序要么无法运行要么行为异常。为什么GCC工具链常输出BIN在Linux环境下.bin通常指纯粹的二进制映像如dd命令生成的镜像而可执行文件是ELF格式。但在嵌入式GCCarm-none-eabi-gcc中我们常通过objcopy命令从ELF生成.bin这是因为许多开源烧录工具如OpenOCD、pyOCD和Bootloader设计更倾向于使用这种无格式的原始二进制数据搭配明确的地址参数更加灵活。3. 转换实战工具、命令与避坑指南理解了原理动手转换就是水到渠成的事情。这里我们聚焦最常用的转换场景和工具。3.1 从ELF到BIN/HEX/SREC使用objcopyobjcopy是GNU Binutils工具集里的瑞士军刀专门用于目标文件的拷贝和转换。它的核心逻辑是从ELF文件中根据链接脚本定义的内存布局提取出需要加载到目标设备内存中的段通常是.text.data.rodata等并按照指定格式输出。基本命令格式arm-none-eabi-objcopy -O 输出格式 输入文件 输出文件1. 生成BIN文件arm-none-eabi-objcopy -O binary input.elf output.bin-O binary指定输出格式为纯二进制。这个过程可以理解为链接器ld根据链接脚本.ld文件生成了ELF它知道每个段应该放在内存的哪个地址。objcopy读取这些信息将那些需要加载到内存类型为LOAD的段如.text.data按照它们的负载内存地址进行排序和拼接地址之间的空隙用0填充最终生成一个连续的二进制块就是.bin文件。关键点生成的.bin文件起始内容对应的是ELF中负载地址最低的那个段。对于STM32这通常是0x08000000开始的.text段。2. 生成HEX文件arm-none-eabi-objcopy -O ihex input.elf output.hex-O ihex指定输出格式为Intel HEX。objcopy会遍历ELF中的可加载段为每一段连续的数据生成一条或多条HEX记录并自动计算校验和。如果地址跨度大比如超过64KB它还会自动插入扩展线性地址记录类型0x04。3. 生成SREC文件arm-none-eabi-objcopy -O srec input.elf output.srec-O srec指定输出格式为Motorola S-Record。高级与排错选项修改入口地址/调整数据objcopy功能强大你甚至可以在转换时修改内容。# 在bin文件开头添加一个2048字节的填充例如用于Bootloader arm-none-eabi-objcopy -O binary --gap-fill0xFF --pad-to0x08000800 input.elf output.bin--pad-to选项会强制输出文件大小达到指定地址不足部分用--gap-fill指定的值填充。这在制作需要预留Bootloader空间的升级包时非常有用。只提取特定段# 只提取.text段生成bin arm-none-eabi-objcopy -O binary -j .text input.elf text_section.bin常见问题生成的BIN文件巨大无比这通常是因为ELF文件中包含了一些非常大的、非加载的调试信息段如.debug*而objcopy的默认行为可能包含了它们。确保你的命令是从可执行ELF文件转换而不是从包含调试信息的ELF文件转换。更常见的巨无霸BIN是因为.data段的初始化数据在ROM中但运行时需要拷贝到RAM而.bss段未初始化数据不占文件空间。如果BIN文件大小远超你的代码预期检查链接脚本和objcopy过程确认没有错误地包含了调试段或填充了过多间隙。3.2 在IDE中配置自动生成手动敲命令太麻烦集成到构建流程里才是正道。Keil MDK进入Options for Target - User选项卡。在After Build/Rebuild部分勾选Run #1。在命令框中输入fromelf --bin --outputL.bin !Lfromelf是ARM工具链自带的工具。--bin指定输出bin格式。--outputL.bin表示输出文件名与目标名相同后缀为.bin。L是Keil的内置变量代表目标名。!L是输入文件即当前构建生成的.axfELF文件。同样可以添加fromelf --i32 --outputL.hex !L来生成HEX文件。--i32表示输出Intel 32位Hex格式。STM32CubeIDE (Eclipse-based)右键项目 -Properties。进入C/C Build - Settings。选择Tool Settings选项卡找到MCU Post build outputs。勾选Convert to binary file和/或Convert to Intel Hex fileIDE会自动在构建后调用arm-none-eabi-objcopy为你生成对应的文件。你还可以在MCU Post build outputs下方的Command line pattern里自定义objcopy的参数。IAR Embedded Workbench进入Project - Options - Output Converter。勾选Generate additional output。在Output format中选择binary或Intel extended等格式。可以指定输出文件路径和文件名。3.3 HEX/BIN/SREC之间的互转有时你可能拿到一个.hex文件但烧录工具只支持.bin或者反过来。这时就需要格式间的直接转换。使用专业的烧录/编程工具大多数功能完善的编程器软件都支持格式互转。J-Flash(SEGGER)File - Open打开一种格式然后File - Save data as...保存为另一种格式在保存对话框中可以选择BinaryIntel HEXMotorola S-Record等。STM32CubeProgrammer在File菜单中也有类似的数据打开和保存功能支持格式转换。pyOCD通过命令行工具pyocd convert可以实现多种格式间的转换。使用命令行工具srec_cat(来自SRecord工具集)这是一个极其强大的工具不仅能转换还能合并、拆分、填充、校验。# 将 HEX 转换为 BIN并指定加载地址假设数据从0x8000000开始 srec_cat input.hex -intel -offset - -minimum-addr 0x08000000 -o output.bin -binary # 将 BIN 转换为 HEX需要指定起始地址 srec_cat input.bin -binary -offset 0x08000000 -o output.hex -intel-offset参数在这里至关重要因为它为没有地址信息的BIN文件赋予了地址。bincopy(Python库)如果你喜欢用脚本处理这是一个很好的选择。import bincopy # HEX转BIN with open(input.hex, r) as f: hex_data f.read() bin_data bincopy.unhexlify(hex_data) # 注意这只会提取数据可能丢失地址间隔信息 # 更完整的处理建议使用srec_cat或objcopy一个关键陷阱地址信息的丢失与重建从.bin转换到.hex或.srec是有损转换的逆过程。因为.bin文件本身没有地址信息所以转换时你必须通过参数如-offset 0x08000000明确指定这个.bin文件内容应该对应的起始内存地址。如果你指定的地址错了生成的.hex文件地址信息就是错的烧录后程序自然无法运行。务必确认原始.bin文件在目标设备上的正确加载地址。4. 进阶话题校验、填充与量产烧录在真实项目尤其是量产环节文件格式转换不仅仅是“能转就行”更要考虑可靠性、效率和兼容性。4.1 校验和的计算与验证校验和是确保数据完整性的重要手段尤其在通过不可靠通道如串口、无线传输固件时。HEX文件校验和如前所述HEX文件每行都有校验和用于验证单行数据。但整个文件的完整性通常需要额外计算。常见的做法是在HEX文件末尾添加一条特殊的校验记录。例如使用0x03类型的记录开始段地址记录存放一个自定义的校验值或者工具在转换时自动添加。一些烧录器在烧录前会验证这个校验和。为BIN文件添加校验和.bin文件本身无结构校验和需要附加在文件内容中或者由烧录协议/ Bootloader来计算。附加在文件尾这是最常见的方式。你可以用一个小脚本计算整个.bin文件的CRC32或SHA256然后将这个校验值通常是4或32字节追加到文件末尾。Bootloader在接收完文件后会重新计算前面数据的校验值并与末尾的进行比较。# 使用Linux命令计算CRC32并附加示例 crc32 firmware.bin checksum.txt # 或者用Python import binascii with open(firmware.bin, rb) as f: data f.read() crc binascii.crc32(data) 0xffffffff with open(firmware_with_crc.bin, wb) as f: f.write(data) f.write(crc.to_bytes(4, little)) # 以小端序附加4字节CRC由烧录器计算一些智能烧录器在发送.bin文件数据流时会按帧计算并发送校验和Bootloader逐帧校验。在线校验工具当你需要快速验证一个HEX文件的校验和或者计算一段数据的CRC时网上有很多在线的“hex文件在线累加校验计算工具”。它们通常允许你粘贴HEX数据或上传文件选择校验算法如CRC-16/32 累加和然后计算出结果。在调试Bootloader通信协议时这类工具非常方便。4.2 地址对齐与空洞填充嵌入式设备的存储空间如Flash并不是所有地址都有效或需要编程。链接后各个段之间可能存在地址间隙。在生成最终的烧录文件时我们需要处理这些间隙。HEX/SREC它们天生支持地址不连续的数据。对于地址间隙这些格式 simply 跳过不产生数据记录。烧录器遇到地址跳变时会自动寻址到下一个位置。这是HEX/SREC格式的一大优势。BINBIN文件是连续的。地址间隙必须被填充。objcopy在生成.bin时默认会用0x00来填充这些间隙。例如.text段在0x08000000-0x0800A000.data段在0x20000000-0x20000200那么生成的.bin文件将从0x08000000开始包含.text段的所有内容然后从0x0800A001到0x20000000之间巨大的地址空间都会被填充为0这会导致.bin文件异常庞大。解决方案使用多段BIN或修改链接脚本生成多个BIN文件针对不同的加载地址区域分别生成BIN文件。# 提取Flash部分 arm-none-eabi-objcopy -O binary -j .text -j .rodata -j .data input.elf flash.bin # 提取RAM部分如果需要单独加载 arm-none-eabi-objcopy -O binary -j .data -j .bss input.elf ram.bin烧录时需要将flash.bin烧到Flash起始地址ram.bin烧到RAM起始地址。这需要烧录器支持多文件烧录或编写特定的烧录脚本。修改链接脚本尽量将需要连续加载的段放在相近的地址减少空洞。或者使用AT指令将.data段的加载地址LMA紧挨着.text段存放在启动代码中再将其拷贝到RAM中。这样生成的.bin文件就不会包含巨大的填充区域了。4.3 量产烧录中的格式选择在工厂量产烧录成千上万的芯片时效率、可靠性和成本是关键。HEX vs BINHEX由于是文本格式文件体积通常比等效的BIN大2-3倍。传输和存储效率低。但优点是自带地址烧录员操作不易出错适合小批量、多品种的生产或者烧录工具比较简单如脱机烧录器直接读U盘里的HEX文件。BIN二进制格式体积最小传输快节省存储空间。是量产的首选。但必须配套明确的烧录地址配置文件通常是一个简单的文本文件如firmware.bin 0x08000000或者烧录软件界面需要手动输入地址。这对生产流程的标准化要求更高。SREC在现代量产中已较少使用除非客户有特殊要求或设备老旧。趋势越来越多的量产烧录方案采用加密的BIN包。将应用程序BIN文件、Bootloader、配置信息等打包成一个加密的容器文件烧录器通过授权认证后才能烧录保护知识产权。这种容器文件内部可能是BIN格式但对烧录器呈现为一个专有格式。5. 典型问题排查为什么我的文件烧录后不运行格式转换和烧录过程中会遇到各种问题这里列举几个最常见的。问题一Keil生成了HEX但我想用BIN怎么设置如3.2节所述在Keil的User选项卡中添加fromelf --bin --outputL.bin !L命令。注意Keil默认的编译输出是.axfELF格式fromelf工具需要正确安装并在系统路径中。问题二从GCC生成的BIN文件烧录到STM32后程序不启动。按照以下步骤排查检查中断向量表STM32启动后首先从0x08000000Flash起始地址读取初始栈指针MSP然后从0x08000004读取复位向量Reset_Handler。确保你的.bin文件是从这个地址开始生成的。用十六进制编辑器打开.bin文件查看前8个字节应该是一个合法的栈地址通常指向RAM末尾和一个函数地址。检查烧录地址你是否在烧录软件中正确设置了.bin文件的烧录起始地址为0x08000000这是最常犯的错误。检查时钟和初始化程序是否在启动后正确初始化了系统时钟HSE/HSI如果没有MCU可能运行在极低的频率下看起来像“死机”。可以在启动的最开头点个灯测试。使用ELF调试尝试烧录.elf或.axf文件如果烧录器支持然后连接调试器看PC指针是否停在Reset_Handler能否单步执行。这是最直接的调试方式。问题三转换后的HEX文件烧录器提示“校验和错误”或“地址溢出”。校验和错误可能是转换工具存在bug或者源文件在传输过程中损坏。用文本编辑器打开HEX文件检查最后几行尤其是结束记录:00000001FF是否正确。可以尝试用其他工具如objcopysrec_cat重新转换一次。地址溢出常见于8位或16位MCU。HEX记录中的地址字段是2字节最大表示64KB地址空间。对于超过64KB的地址需要使用扩展线性地址记录类型0x04。如果转换工具没有正确生成这些扩展记录当程序地址超过0xFFFF时烧录器就会报错。确保你使用的objcopy或转换工具支持生成32位地址的HEX格式-O ihex默认支持。问题四我想查看HEX文件里特定地址的数据怎么做不要用文本编辑器肉眼找。使用命令行工具# 使用 grep 配合 srec_cat (SRecord工具集) srec_cat your_file.hex -intel -crop 0x08001000 0x08001010 -o - -hex-dump这个命令会提取0x08001000到0x0800100F地址范围内的数据并以十六进制格式打印出来。objdump也可以用来反汇编ELF文件但直接解析HEX文件不太方便。理解这些可执行文件格式的转换就像是掌握了嵌入式开发的“物流语言”。它让你能在编译器、烧录器、调试器和芯片之间自由地搬运程序代码确保每一份心血都能准确无误地抵达目的地。从搞清楚HEX每一行记录的含义到熟练运用objcopy处理各种边界情况再到为量产选择最合适的格式每一步都凝结着对系统底层运作的深刻理解。下次当你点击“Build”后不妨花点时间看看输出文件夹里那些不同后缀的文件想想它们各自的旅程和使命这会让你的开发工作更加得心应手。
返回列表