嵌入式开发必备:ELF、BIN、HEX、SREC文件格式转换原理与实战

📅 2026/8/6 10:12:11
嵌入式开发必备:ELF、BIN、HEX、SREC文件格式转换原理与实战
1. 项目概述嵌入式开发中的“翻译官”在嵌入式开发的日常里我们经常要和各种格式的可执行文件打交道。你肯定遇到过这样的场景Keil 或者 IAR 编译完默认给你一个.hex或者.out文件但你的烧录工具偏偏只认.bin或者你想分析一个现成的固件拿到手的是一个.srec文件需要转换成更通用的格式才能用工具打开。这些.hex、.srec、.elf、.bin文件就是我们程序代码的最终形态但它们长得不一样用途也各有侧重。简单来说这个过程就像把一份用中文写的菜谱源代码经过厨师编译器烹饪变成了一道色香味俱全的菜机器码。但为了适应不同的上菜方式烧录器和食客需求调试、分析我们需要把这道菜装进不同的餐盘里。.elf是带着精美摆盘和食材清单的完整套餐.bin是去掉所有装饰、只保留核心食材的纯肉块.hex和.srec则是标好了每个肉块应该放在餐盘哪个位置的“坐标地图”。这个项目要解决的就是如何在这些“餐盘格式”之间自由、准确地进行转换。这不仅仅是点一下“另存为”那么简单你需要理解每种格式的“语法”和“语义”知道转换时会丢失什么、保留什么以及如何避免在转换过程中引入错误。对于嵌入式软件工程师、固件逆向分析人员甚至是玩硬件的极客来说掌握这套“翻译”技能是打通开发、调试、生产、维护各个环节的基础。接下来我们就深入拆解这几种常见格式并手把手带你搞定它们之间的转换。2. 核心文件格式深度解析在动手转换之前我们必须先搞清楚我们要处理的“原材料”和“目标成品”到底是什么。每种文件格式都是为了解决特定问题而设计的理解其结构是避免转换错误的前提。2.1 ELF带调试信息的“完整工程文件”ELFExecutable and Linkable Format文件是Linux/Unix系统和许多嵌入式工具链如GCC ARM默认生成的目标文件格式。你可以把它理解为一个结构非常清晰的“项目文件夹”。ELF文件的核心结构包含以下几个部分ELF Header文件头相当于文件夹的索引指明了文件类型可执行文件、可重定位文件等、目标机器架构ARM、x86等、程序入口地址以及后续节区头表Section Header Table和程序头表Program Header Table的位置和大小。Program Header Table程序头表仅存在于可执行文件和共享库中。它描述了系统加载器如何将文件映射到进程的虚拟内存中。每个表项Program Header定义了一个段Segment如可加载的代码段.text、数据段.data等。Section Header Table节区头表描述了文件中的所有节区Section。节区是链接和重定位的基本单位例如.text存放已编译的机器指令代码。.data存放已初始化的全局变量和静态变量。.bss存放未初始化的全局变量和静态变量在文件中不占空间仅记录大小。.rodata存放只读数据如常量字符串。.symtab符号表存放函数和变量名及其地址。.debug_*各种调试信息如行号、局部变量等。.strtab字符串表为符号表等提供字符串存储。关键点与转换影响当我们从ELF转换到其他格式如BIN时通常只提取需要被加载到目标设备内存中执行的段Segment而丢弃符号表、调试信息等辅助节区。这也是为什么ELF文件通常比BIN文件大得多。objcopy工具在转换时就是依据程序头表来决定提取哪些内容的。2.2 BIN纯粹的“内存镜像”BIN文件是最原始、最直接的二进制内存镜像。它不包含任何地址信息、校验和或格式标记。文件的内容就是从某个起始地址开始按顺序排列的二进制数据流准备被原封不动地烧录到Flash或RAM的对应位置。特点与风险无元数据BIN文件本身无法告诉你它应该被烧录到哪个地址。这个信息必须由使用者从其他途径如链接脚本、ELF文件头或项目配置获知并在烧录时手动指定。这是BIN文件烧录出错的最常见原因——地址搞错了。紧凑由于没有额外信息BIN文件体积最小非常适合用于量产烧录或OTA升级可以节省传输带宽和存储空间。“盲”文件没有符号和调试信息无法直接用于源码级调试。注意正因为BIN文件“赤裸”在转换和烧录时起始地址Load Address/Origin是必须明确指定的参数绝对不能遗漏。2.3 HEX带地址的“文本化”二进制Intel HEX格式是一种用ASCII文本字符来表示二进制数据的格式。它由一条条“记录Record”组成每条记录独立成行结构清晰。一条典型的HEX记录如下:10010000214601360121470136007EFE09D2190140: 记录起始标志。10 本行数据字节长度16字节。0100 本行数据起始的16位地址0x0100。00 记录类型。00表示数据记录01表示文件结束记录02表示扩展段地址记录04表示扩展线性地址记录用于32位地址。214601360121470136007EFE09D21901 实际的二进制数据用16进制ASCII码表示。40 校验和Checksum用于验证该行数据的完整性。优势与应用场景自包含地址数据记录自带偏移地址扩展地址记录能处理大容量存储。因此HEX文件在烧录时通常不需要额外指定地址烧录器会解析这些记录并写入正确位置。易于查看和编辑因为是文本格式可以用任何文本编辑器打开查看甚至进行简单的手动修改需重算校验和。兼容性好是历史上非常流行的格式被大量古老的编程器和工具支持。空间效率低用两个ASCII字符表示一个字节数据文件体积约为原始二进制的2倍。2.4 SREC摩托罗拉版的“HEX”SRECS-Record格式由摩托罗拉制定与Intel HEX在理念上相似都是文本化的、带地址的二进制数据格式但语法不同。一条典型的SREC记录如下S1131000285F245F2212226A000424290008237C2AS1 记录类型。S0头记录S116位地址数据记录S224位地址数据记录S332位地址数据记录S5记录数S7/S8/S9终止记录。13 本记录字节总数地址数据校验和这里是0x13即19字节。1000 本行数据起始地址0x1000。285F245F2212226A000424290008237C 数据。2A 校验和。与HEX的对比地址范围SREC通过S1/S2/S3直接支持16/24/32位地址而HEX需要通过特殊记录类型02/04来扩展SREC在处理不同位宽地址时语法更统一。头部信息SREC的S0记录可用于存储文件头描述信息HEX没有标准化的文件头。流行度在嵌入式领域HEX可能更常见尤其是8051、ARM Keil环境而SREC在某些处理器如一些PowerPC、ColdFire和工具链中更流行。两者功能上基本可以互相替代。3. 转换工具链与实战操作理解了格式我们来看看如何用工具进行转换。这里主要介绍最通用、最强大的工具链GNU Binutils 中的objcopy以及一些其他常用工具。3.1 瑞士军刀GNU objcopyobjcopy是转换工作的核心。它专门用于复制和转换目标文件。其基本转换逻辑是从一个包含丰富信息如ELF的文件中提取出指定的部分通常是需要加载的段输出为另一种格式。1. ELF 转 BIN这是最常用的转换。你需要明确指定输出的起始地址或者更常见的是让objcopy根据ELF文件中的程序头可加载段自动提取。# 最常用的方式提取所有需要加载的段loadable sections arm-none-eabi-objcopy -O binary input.elf output.bin # 如果需要指定只提取某个段例如.text段代码段 arm-none-eabi-objcopy -O binary -j .text input.elf code.bin # 更精细的控制指定输入目标文件格式和输出格式 arm-none-eabi-objcopy --input-targetelf32-littlearm --output-targetbinary input.elf output.bin-O binary 指定输出格式为纯二进制BIN。-j .text 只提取名为.text的节区。你可以使用多个-j选项来提取多个节区。关键点当使用-O binary且不指定-j时objcopy会创建一个输出文件它包含了所有具有“可加载”标志且地址连续的段在内存中的镜像。如果这些段之间存在地址间隙Gap这些间隙在BIN文件中通常会被填充为0取决于链接脚本和工具行为以确保输出文件的布局与内存映射一致。这解释了为什么有时BIN文件会比实际代码数据大。2. ELF 转 HEXarm-none-eabi-objcopy -O ihex input.elf output.hex-O ihex 指定输出格式为Intel HEX。3. ELF 转 SRECarm-none-eabi-objcopy -O srec input.elf output.srec-O srec 指定输出格式为Motorola S-record。4. HEX/SREC 转 BIN虽然objcopy可以直接处理HEX/SREC作为输入但更常见的流程是从源码编译得到ELF再转BIN。如果只有HEX/SREC可以# 将HEX转换为ELF一个中间格式再转BIN arm-none-eabi-objcopy -I ihex -O elf32-littlearm input.hex temp.elf arm-none-eabi-objcopy -O binary temp.elf output.bin # 或者一步到位某些版本objcopy支持 arm-none-eabi-objcopy -I ihex -O binary input.hex output.bin --gap-fill 0xFF-I ihex 指定输入格式为Intel HEX。--gap-fill 指定地址间隙的填充值对于Flash设备填充0xFF是常见做法因为擦除后的Flash值为0xFF。3.2 集成开发环境IDE内的转换对于开发者来说在IDE中一键生成所需格式更为便捷。Keil MDKKeil默认生成.axfELF格式的变种和.hex文件。要生成.bin文件通常需要在Options for Target - User选项卡中配置After Build/Rebuild的运行命令。使用Keil自带的fromelf.exe工具ARMCC工具链或调用arm-none-eabi-objcopy如果使用GCC插件。ARMCC示例fromelf --bin --outputL.bin !LGCC ARM示例arm-none-eabi-objcopy -O binary L.axf L.binSTM32CubeIDE (基于Eclipse/GCC)STM32CubeIDE使用GCC工具链生成.elf文件。生成.hex和.bin是内置选项右键项目 -Properties。C/C Build - Settings。在Tool Settings标签页找到MCU Post build outputs。勾选Convert to Intel Hex file (-O ihex)和Convert to binary file (-O binary)。IDE会自动在构建后调用objcopy完成转换。IAR Embedded WorkbenchIAR默认生成.out其自定义格式类似ELF和.hex文件。生成.bin文件进入Project - Options - Output Converter。勾选Generate additional output。在Output format中选择Binary。可以指定文件名称和地址范围。3.3 其他实用工具与脚本xxd Linux/Unix自带的十六进制转储工具可以用于简单的格式查看和转换。# 将BIN文件转换为C语言数组形式的HEX文本 xxd -i firmware.bin firmware.h # 将HEX文本转换回BIN需要先处理掉C数组格式 xxd -r -p hexdata.txt firmware.binsrec_cat 来自SRecord工具包功能极其强大专精于HEX/SREC/BIN等各种格式的转换、拼接、填充、校验和计算等。# 合并两个BIN文件第一个放在0x0000第二个放在0x8000间隙填0xFF srec_cat file1.bin -binary -offset 0x0000 file2.bin -binary -offset 0x8000 -o merged.hex -intel # 将HEX文件转换为BIN并指定填充值 srec_cat input.hex -intel -o output.bin -binaryPython脚本 对于复杂的、定制化的转换逻辑例如在二进制数据中插入特定的头结构、进行加密或压缩编写Python脚本是最灵活的方式。可以使用intelhex、srecord等第三方库来方便地解析和生成HEX/SREC文件。4. 转换过程中的核心问题与解决方案格式转换并非总是点一下按钮那么简单在实际操作中会遇到各种“坑”。下面是一些典型问题及其解决方法。4.1 地址错位BIN文件烧录失败的首因问题现象使用BIN文件烧录后设备无法运行或者运行行为异常。根本原因BIN文件没有内部地址信息。烧录工具需要你告诉它“把这个文件的内容从Flash的哪个地址开始写”。如果这个起始地址Load Address设置错误代码和数据就被放错了地方CPU自然无法正确执行。解决方案从源头确认查看你的链接脚本.ld文件或IDE中的配置找到代码段通常是.text的起始地址VMA即虚拟内存地址。例如在STM32中Flash起始地址通常是0x08000000。从ELF文件获取使用objdump或readelf工具查看ELF文件头。arm-none-eabi-objdump -f input.elf # 输出中会包含 start address 0x08000000 这样的信息 arm-none-eabi-readelf -l input.elf | grep LOAD # 查看可加载段的地址和大小在烧录工具中正确设置在烧录软件如J-Flash、STM32CubeProgrammer、OpenOCD中将“起始地址”、“偏移地址Offset”或“基地址Base Address”设置为正确的值如0x08000000。使用HEX/SREC文件如果你觉得管理地址很麻烦直接使用HEX或SREC文件进行烧录是更稳妥的选择因为地址信息内嵌在文件中。4.2 空洞填充为什么BIN文件比想象中大问题现象代码只有100KB但生成的BIN文件有512KB。原因分析在链接脚本中不同段.text,.data,.rodata之间可能存在地址间隙。例如.text 起始于 0x08000000 长度 90KB .data 起始于 0x08020000 长度 10KB.text和.data之间有一个约64KB的间隙。当使用objcopy -O binary时默认行为是生成一个从第一个可加载段地址开始到最后一个可加载段地址结束的连续镜像。这个间隙也会被包含在BIN文件中并用0或其他指定值填充。影响与处理影响BIN文件变大烧录时间变长占用更多存储空间。处理接受它对于大多数应用这点空间可以接受且能保证内存布局的准确性。优化链接脚本重新安排段的位置减少间隙。生成多个BIN文件使用objcopy -j选项为不同地址区域生成独立的BIN文件分别烧录。使用HEX/SRECHEX/SREC格式只记录有数据的地址自动跳过间隙文件体积更接近有效数据总和。4.3 校验和与完整性验证问题现象文件转换或传输后烧录进去的程序无法运行怀疑数据在中间过程出错。重要性在量产、OTA升级等场景确保二进制文件的完整性至关重要。解决方案利用格式自带校验HEX和SREC文件的每一行都有校验和可以初步验证文件是否被意外修改。一些工具如hex2bin在转换时会验证校验和。计算哈希值在转换前后分别计算ELF/BIN文件的哈希值如MD5、SHA256进行比对这是最可靠的验证方法。# Linux/macOS md5sum firmware.bin sha256sum firmware.elf # Windows (PowerShell) Get-FileHash -Algorithm SHA256 firmware.bin添加自定义校验在固件末尾追加一个CRC32或校验和字段。这需要在链接脚本中预留空间并在代码中实现校验算法。转换工具不会破坏这部分数据。使用srec_cat工具它可以在转换过程中计算并添加校验和。srec_cat input.bin -binary -crop 0 0x10000 -fill 0xFF 0x0000 0x10000 -output_checksum_negative_little_endian 0x10000 4 -o output_with_crc.srec -srec4.4 调试信息的剥离与保留问题场景用于生产的固件需要尽可能小而用于调试的固件需要包含符号信息。操作剥离调试信息生产发布的ELF文件通常需要剥离调试信息以减小体积。arm-none-eabi-strip --strip-debug input.elf -o output_release.elf # 然后再将 output_release.elf 转换为 BIN/HEX保留调试信息用于调试的ELF文件必须保留所有调试节区。使用objcopy转换时默认不会包含调试信息到BIN/HEX中因为它们是给调试器用的不是给CPU执行的。调试信息通常单独存放在.elf文件中或使用DWARF格式。实操心得建立清晰的构建流水线。例如在CI/CD中一个构建任务可以同时产生三个产物firmware.elf带调试信息用于开发、firmware.elf剥离后用于存档、firmware.bin/firmware.hex用于烧录生产。使用不同的文件后缀或输出目录来区分它们。5. 高级应用与自动化脚本示例当项目变得复杂或者需要融入自动化流程时手动点击IDE按钮或执行单条命令就显得力不从心了。这里分享几个实用的高级场景和自动化脚本思路。5.1 多段二进制文件的合并与拆分在一些复杂系统中Bootloader和Application可能分别编译生成两个独立的BIN文件但需要合并成一个文件进行烧录。使用srec_cat合并假设Bootloader.bin烧录到0x08000000App.bin烧录到0x08010000。srec_cat bootloader.bin -binary -offset 0x08000000 \ application.bin -binary -offset 0x08010000 \ -o combined.hex -intel这个命令将两个BIN文件按指定地址偏移合并并输出为一个HEX文件。地址间隙会自动处理。使用dd命令拆分如果你有一个完整的BIN镜像想提取其中一部分例如从0x1000偏移处提取2KB数据dd iffull_image.bin ofextracted_part.bin bs1 skip$((0x1000)) count$((2*1024))5.2 为二进制文件添加自定义头结构在OTA升级或安全启动中经常需要在固件二进制文件前添加一个描述头包含版本号、大小、CRC、加密签名等信息。Python脚本示例import struct import binascii def add_header_to_bin(input_bin_path, output_bin_path, version_major1, version_minor0): with open(input_bin_path, rb) as f: firmware_data f.read() # 1. 计算固件CRC32 (作为示例) firmware_crc binascii.crc32(firmware_data) 0xFFFFFFFF # 2. 定义头结构魔数(4B) 版本(2B) 固件大小(4B) CRC32(4B) 14字节 # 假设魔数为 0xDEADBEEF header_format I H I I # 小端字节序 magic 0xDEADBEEF fw_size len(firmware_data) header_data struct.pack(header_format, magic, (version_major 8) | version_minor, fw_size, firmware_crc) # 3. 将头和固件数据拼接 with open(output_bin_path, wb) as f: f.write(header_data) f.write(firmware_data) print(fHeader added. Firmware size: {fw_size}, CRC32: {hex(firmware_crc)}) # 使用 add_header_to_bin(app_raw.bin, app_with_header.bin, 1, 2)这个脚本创建了一个14字节的头然后将其与原始BIN文件拼接。在Bootloader中需要按照同样的格式解析这个头验证魔数和CRC然后再跳转到应用程序执行。5.3 集成到Makefile自动化构建对于使用GCC和Makefile的项目将格式转换步骤集成到构建流程中是标准做法。示例Makefile片段# 工具链前缀 CROSS_COMPILE arm-none-eabi- OBJCOPY $(CROSS_COMPILE)objcopy OBJDUMP $(CROSS_COMPILE)objdump SIZE $(CROSS_COMPILE)size # 目标文件 TARGET my_firmware # 构建目标同时生成elf, hex, bin, 并反汇编 all: $(TARGET).elf $(TARGET).hex $(TARGET).bin $(TARGET).lst # 链接生成elf $(TARGET).elf: $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $ $^ echo 生成ELF文件 $(SIZE) $ # 从elf生成hex %.hex: %.elf echo 生成Intel HEX文件: $ $(OBJCOPY) -O ihex $ $ # 从elf生成bin (关键步骤) %.bin: %.elf echo 生成纯二进制文件: $ $(OBJCOPY) -O binary -S $ $ echo 提示BIN文件的烧录起始地址为: 0x08000000 (请根据链接脚本确认) # 生成反汇编列表便于调试 %.lst: %.elf $(OBJDUMP) -S -d $ $ # 清理 clean: rm -f $(OBJS) $(TARGET).elf $(TARGET).hex $(TARGET).bin $(TARGET).lst在这个Makefile中执行make all会依次生成.elf、.hex、.bin和.lst反汇编文件。.bin的生成规则是重点它使用了-O binary和-S移除所有符号和重定位信息选项。同时通过echo提示了烧录地址这是一个很好的实践。5.4 不同格式的查看与快速分析查看ELF文件结构arm-none-eabi-readelf -a firmware.elf | less # 使用 -S 查看节区头-l 查看程序头段信息-s 查看符号表查看HEX/SREC文件内容# 使用文本编辑器或cat直接查看 cat firmware.hex | head -20 # 使用专门的工具解析 srec_info firmware.srec反汇编BIN文件由于BIN文件没有符号信息反汇编需要指定架构和起始地址。arm-none-eabi-objdump -D -b binary -marm firmware.bin --start-address0x08000000 | less-D: 反汇编所有段。-b binary: 指定输入格式为二进制。-marm: 指定目标架构为ARM。--start-address:至关重要告诉反汇编器代码的起始地址这样才能正确解析ARM/Thumb指令因为有些指令集状态取决于地址最低位。掌握这些文件格式的转换与操作就如同掌握了嵌入式软件交付的“最后一公里”。从编译器产生的原始ELF到适合烧录的HEX/BIN再到用于分析的中间格式每一步都蕴含着对硬件内存布局和软件执行逻辑的深刻理解。避免地址错位、处理数据间隙、保证文件完整性这些细节决定了固件能否在目标板上可靠运行。希望这篇详尽的梳理能让你在下次面对格式转换问题时不再感到困惑而是能够游刃有余地选择最合适的工具和方法高效地完成工作。