资讯动态

嵌入式固件格式详解:从axf到hex和bin的转换与调试

发布时间:2026/9/30 23:31:05 来源:尧图企业网站定制
如果说嵌入式开发有什么“看起来简单、一面试就卡壳”的基础题hex、bin、axf这三个文件绝对排得上号。很多人天天在Keil里点编译工程目录下冒出一堆文件只认识那个.hex看到.axf还以为是IDE生成的临时垃圾遇到.bin也不知道什么时候用。结果到了产线要固件给烧录器报个错或者程序跑飞了看着反汇编一脸茫然根本不知道还要用axf去定位。今天我就把这三兄弟从格式定义到实际落地完整讲一遍争取让你看完之后不管是调试、转格式、做产线包都能自己搞定。无论你搞STM32、瑞萨还是嵌入式Linux底层固件这套“文件系统”逻辑都是相通的。1. 从一次编译说起axf、hex、bin到底从哪里冒出来的1.1 编译、链接、产物的三个阶段先理清一个最基础的问题Keil里点一下Build计算机到底做了什么事流程不是“C语言源码直接变成hex”中间其实隔了好几道工序。第一步是编译每个.c文件被编译器翻译成对应的.o目标文件里面是机器指令和数据的“半成品”符号还没有定死。第二步是链接链接器把所有.o文件和用到的库合并到一起重定位各个函数和变量的地址最后生成一个完整的可执行映像。这个映像在ARM Keil环境里就是.axf文件在GCC工具链里通常叫.elf两者本质上都是ELF格式只是扩展名不同、私有段略有差异。axf生成之后我们通常还需要把它转换成烧录文件。怎么转用ARM官方的fromelf工具或者用GNU的objcopy。从axf里剥离掉调试信息、符号表、文件路径这些“不必要”的东西再按不同的编码规则排列就得到hex或bin。所以三者关系可以这样记axf是母体hex和bin是母体派生出的“交付形态”。很多新手以为hex是源码的直接产物其实源码到axf才是真正的编译链接hex和bin只是“导出格式”。1.2 axf是ELF的裁缝版调试信息都藏在哪为什么调试器加载的是axf而不是hex因为axf里包含了调试所需的“全息档案”。它里面有程序段text、数据段data、BSS段、堆栈段还有一大堆符号表。符号表干的事就是把0x08001234这种地址翻译成main、uart_send_char之类的函数名把0x20000010翻译成uart_rx_buffer这种变量名。编译时如果你开了-g或者Keil的Debug Information选项axf里甚至保存了每个函数对应的源码行号。有了这套信息调试器才能实现三件关键事情第一设置源码级断点你在IDE的C语言行号旁边点一下它把目标PC地址写进去第二查看局部变量和全局变量的实时值因为符号表知道变量地址、类型和内存布局第三调用栈回溯从PC和LR寄存器一路还原出func_a - func_b - main的调用链。hex和bin里这些信息全部被剥离了所以拿着bin文件最多只能反汇编没法像axf那样做源码级调试。以前帮同事调一个问题他拷了个hex让我看看哪里跑飞我打开反汇编窗口满屏都是0x0800xxxx没有符号查起来像大海捞针。1.3 hex与bin的分工一个带地址、一个纯裸奔hex文件从外表看是个纯文本文件用记事本打开能看到一行行以冒号开头的ASCII字符比如:020000040800F2。它的特点是每一行都携带有地址信息烧录器读到哪一行就知道这一行数据该写到哪个Flash地址。所以hex文件有“自描述”能力即便固件的代码段、数据段分散在不连续地址也能用一条条记录表达清楚。bin文件则走另一个极端它没有任何地址信息也不存在“一行行记录”这种逻辑就是一个从某个起始地址开始连续排列的裸二进制镜像。想象一个白板你刷刷刷在上面写满了字节至于白板该挂在哪面墙上bin不告诉你。这个“挂在哪儿”的工作必须由烧录工具、下载算法或者用户手动输入的起始地址来完成。因此bin文件比hex更纯粹、更紧凑但也更容易被用错地方——起始地址填错程序就等于写到了错误的位置。后面我会专门讲这个坑。2. 别被hex的文本外表骗了逐行拆解一条HEX记录2.1 HEX记录的六种类型00到05分别管什么Intel HEX格式是Motorola S19之外最主流的固件文本格式。每一行记录由六个部分拼接而成起始冒号、数据长度1字节、地址2字节、记录类型1字节、数据N字节、校验和1字节。其中记录类型决定了这一行的用途常用的是00、01、04但完整规范里总共有六种类型代码含义作用00数据记录携带真正的烧录数据01文件结束记录标志整个文件到此结束02扩展段地址记录段地址扩展极少见03起始段地址记录指定CS:IP入口多用于x8604扩展线性地址记录高16位地址扩展最常用05起始线性地址记录指定程序入口地址可选绝大多数MCU项目里你只会看到00、01、04这三种。00负责写数据01表示文件搞定04负责告诉我们前面数据记录里地址的高16位。为什么需要04因为记录里的地址字段只有16位最多覆盖64KB空间而STM32的Flash地址是0x08000000一个64KB窗口根本装不下所以必须在切换地址区间时先发一条04记录把高16位地址设好后面紧跟着的00记录里的16位地址才能组合成一个完整的32位地址。2.2 手算校验和验证一条HEX记录的正确性看hex文件时每行最后一个字节是校验和很多工具会自动校验但作为工程师最好知道它是怎么算出来的这样遇到烧录中途报“校验失败”时自己能快速判断是不是文件本身被破坏了。规则很简单把这一行里除了起始冒号和校验和本身之外的所有字节累加起来取低8位然后用0x100减去这个低8位得到的结果就是校验和。我们拿最常见的扩展线性地址记录020000040800F2举例数据长度是0x02地址是0x0000类型是0x04数据是0x08、0x00把这些全部求和0x02 0x00 0x00 0x04 0x08 0x00 0x0E。然后0x100 - 0x0E 0xF2正好和文件末尾的F2对上说明这一行完好。再看一条数据记录比如:03000000010203F7长度0x03地址0x0000类型0x00数据01 02 03。累加0x03 0x00 0x00 0x00 0x01 0x02 0x03 0x090x100 - 0x09 0xF7正确。很多下载器在烧录时会逐行校验如果某一行由于文本编辑不当、换行符被改坏、或文件传输用了二进制模式导致字节错乱校验和会直接暴露问题。这也是为什么我建议大家不要轻易用记事本去“修改”hex文件哪怕只改了一个字符这一行的校验和就废了。2.3 扩展地址记录为什么烧录器能正确找到Flash位置回到STM32的例子。Flash起始地址是0x08000000hex文件里怎么表示先发一条:020000040800F2说明接下来所有数据记录都要在当前的16位地址上叠加0x0800的高16位。紧接着的数据记录:0400000001020304F2地址字段是0x0000组合起来就是0x08000000数据就写在这个位置。如果程序还有一段要放到0x08001000那么前面必须先重新发一条04记录把高16位改成0x0800不变的话可以不用重复或者直接用低16位0x1000去组合。如果代码跨到0x08010000这个地址高16位变了就必须先发一条04记录设成0x0801。理解这一点后你就能解释一个怪异现象为什么用某些工具从hex转bin后bin文件前面总是多出一段0xFF填充因为hex表达的是“有地址的数据”而bin要求“连续从某地址开始排”地址跳变处只能用填充字节补上。所以我常用srec_cat来做转换它可以控制填充值以及是否压缩空洞后面会讲。2.4 hex转十进制、hex反编译C语言这两个常见的误解网上经常有人搜“hex转十进制”我猜他们大概是看到hex文件里全是0A、FF这种字符想转成十进制看看里面写了什么。但实际上这是个误解hex文件中的地址和数据本身就用十六进制表示编译器生成时就是这样你把它“转成十进制”并不会得到更有意义的源码反而会破坏文件结构。你真正需要做的是看明白每一行每个字段的含义而不是逐个字节换进制。比如看到:03000000010203F7里面的01、02、03就是十六进制字节对应十进制的1、2、3想直观理解直接看数据段即可。还有人问“hex文件反编译成C语言”我在这里明确泼一盆冷水不可能。编译过程已经把变量名、函数名、注释、循环结构全部打散成了机器指令hex里连一条符号都没有再厉害的反汇编器也只能还原出汇编指令而且要还原出接近可读的函数逻辑还需要人工参与和分析大量上下文。真正的嵌入式安全分析人员做的是“从汇编到伪代码”的过程那是极费精力的逆向工程不是一键转换。如果你只是想把别人的固件偷出来改成自己的产品趁早放弃这个方向又费时间又容易踩法律红线。3. bin文件没有地址那它靠什么定位3.1 烧录器如何知道bin放到哪起始地址是“外部约定”bin文件是“裸数据”它没有地址那烧录器怎么知道把第一个字节写到0x08000000还是0x08010000答案是用户必须告诉它。无论是J-Flash、STM32CubeProgrammer还是厂里的离线烧录器你在界面里看到的“Base Address”或“起始地址”就是干这个用的。举个具体例子STM32F103系列Flash从0x08000000开始如果你的bin文件是从这个地址导出的烧录时起始地址就必须填0x08000000这里的第一条指令就是启动代码。如果你把起始地址错填成0x08010000芯片上电后从0x08000000读取第一条指令读到的是一堆0xFF轻则跑飞重则直接进入HardFault。再比如做OTA升级时APP的bin通常起始于0x08010000你要把bin整体拷贝到SD卡或通过YModem传输 Bootloader会把bin写入0x08010000那么传输这个bin之前你一定不能“自作聪明”地给它加上头纯数据才是协议最喜欢的格式。3.2 三种最常用的bin生成方法fromelf、objcopy、srec_cat第一种是在Keil里用ARM的fromelf命令。Keil安装目录下有个fromelf.exe专门用来把axf转成各种格式。在工程配置的User页签After Build/Rebuild框里添加一行fromelf.exe --bin -o $LL.bin $LL.axf其中$LL是Keil的宏表示输出文件路径和文件名不带扩展名。加这行命令后每次编译完就会自动生成一个跟axf同名的bin文件。如果你还想顺便生成反汇编列表可以再加一行fromelf.exe --text -c -o $LL.lst $LL.axf。第二种是在GCC环境下用objcopy。嵌入式Linux和许多MCU项目用arm-none-eabi-gcc编译链接后生成的是elf文件转bin的命令是arm-none-eabi-objcopy -O binary firmware.elf firmware.bin注意这句话的意思是把elf里的加载段数据按顺序导成裸二进制。如果elf里有多个分散的加载段objcopy会按内存布局填充空洞生成的文件可能比你预期的更大。这时可以加--gap-fill 0xFF控制填充字节或者考虑不转bin直接用hex。第三种是通过srec_cat把hex转成bin。像这样srec_cat firmware.hex -intel -o firmware.bin -binary这条命令解析Intel HEX里的地址信息按从最小地址到最大地址的顺序导出数据。如果hex里有地址空洞srec_cat默认用0xFF填充。这个工具特别适合“手上只有hex客户非要bin”的尴尬场景。3.3 空片、分区、bootloader场景下bin的优势与坑bin最大的优势是简单、紧凑、无需解析。它的加载速度比hex快得多尤其大固件比如几百KB在UART升级时逐字节发送bin比让接收方解析hex文本要省不少时间和开销。所以在bootloader升级、OTA升级、产线批量烧录这三种场景里大家普遍偏好bin。但bin也有两个让人咬牙切齿的坑。一是没有地址前面已经说过二是它无法表达“多个不连续的地址段”。如果你的程序包含初始化数据要拷贝到RAM、或者有多个加载域bin强行转换后会把所有数据排成一段烧录到Flash后还需要额外的启动代码主动拷贝乱排地址会导致启动过程彻底混乱。遇到这种复杂内存布局最好乖乖用hex或s19它们能精确记录每段数据的目标地址。3.4 别把“同名不同命”的bin混为一谈网上搜“bin文件转sfc”“bin格式音乐转mp4”的人大概率不是在搞嵌入式。bin这个后缀名太泛滥了游戏ROM、光盘镜像、数据库文件、某些软件的资源包都习惯叫bin但它们和固件bin完全是两码事。嵌入式固件的bin是处理器可直接执行的机器码序列没有文件头没有魔数也没有规范内置。你用记事本打开它看到的可能是乱码也可能某个区域恰好对应一串ASCII。如果你拿一个游戏ROM改名成firmware.bin去烧录MCU结果只有一个板子点不亮。我遇到过产线同事把校验用的CRC文件跟固件bin搞混烧进去后整批板子开不了机查了大半天才发现是文件选错。所以在项目里最好约定从开始就用“固件bin”这个称呼并且规定产线文件命名必须带项目名、版本号和日期比如project_app_v1.2.3_20250915.bin能从源头减少这类事故。4. axf不只是调试文件跑飞现场的救命线索4.1 在Keil里加载axf调试到底加载了什么Keil进入调试模式时默认加载的是工程生成的axf而不是hex或bin。为什么因为axf能提供“地址到源码”的映射。你在C文件第58行打了个断点Keil会根据axf里的调试信息把这个行号换算成Flash地址再通过调试器写一个BKPT指令或硬件断点寄存器。你鼠标悬停在变量上能显示当前值也是符号表在背后支撑。很多人遇到这种场景工程没有勾选“Debug Information”或者编译时优化开得太高结果在调试器里看不到变量名、函数名只剩汇编代码行断点也打不上。这不是axf坏了而是编译时没把调试信息写进去。Keil MDK里默认是用AC6编译器时需要勾选Debug InformationGCC环境下要加-g另外编译优化级别建议调试阶段用-O0或-Og否则变量被优化掉断点位置和实际执行可能对不上。4.2 定位HardFault从寄存器、反汇编到源文件程序跑飞最常见的终点是HardFault。这时axf就是你最好的侦察员。第一步进调试器后在Peripherals里打开Core Registers窗口看R14(LR)和R15(PC)。PC保存着出错的指令地址比如0x0800456E。第二步打开Disassembly窗口在地址栏输入0x0800456E回车就能看到出错位置的反汇编指令判断是在执行什么操作时炸的。第三步利用axf里的符号表在Disassembly窗口的反汇编代码旁边通常会标注对应的函数名比如HardFault_Handler、uart_write、memset等等这时就能立刻知道被哪个函数拖下水了。如果工程开了源码级调试Call Stack窗口还能直接显示函数调用链比如main调用了protocol_parse然后protocol_parse里调用了一个野指针。我见过很多堆栈被破坏的情况Call Stack也未必可靠但PC和LR寄存器永远是硬道理。更狠一点的做法是在HardFault_Handler里手动保存现场把R0-R3、R12、LR、PC、PSR都拉出来通过串口打印或存到RAM里一般能精确定位到罪魁祸首。4.3 axf文件报错常见原因与排查思路热词里“axf文件报错”被搜爆了我自己也踩过不少次。先说明一点Keil里所有关于axf的报错多半不是“这个axf文件本身坏了”而是工具链或环境不匹配。最常见的三种情况第一种同一个工程换了编译器版本比如从AC5切到AC6之前生成的axf可能加载不了。因为AC5和AC6使用的ELF细节和调试信息格式有差异调试时Keil可能报“cannot load file, invalid ELF header”之类的错。解决办法很简单Project - Clean Target然后再重新Build生成匹配当前编译器版本的axf。第二种工程路径含中文、空格或者特殊符号fromelf或者调试器在解析路径时出错。虽然现代Keil对中文路径容忍度高了不少但为了省事我依然建议所有嵌入式工程都放纯英文路径下。第三种axf和当前代码不是同一次编译的产物比如你改了代码忘了编译直接加载旧的axf去调试看到的行号、变量值全是偏的排查错误时容易被误导。4.4 从axf生成bin/hex的一键配置在实际项目里每次编译完都手动敲一次fromelf命令太麻烦。我更倾向于把转换命令写进Keil的User页签。具体做法在Keil的Options for Target - User里勾选After Build/Rebuild - Run #1然后在命令行里输入fromelf.exe --bin -o $LL.bin $LL.axf fromelf.exe --i32 -o $LL.hex $LL.axf第一行生成bin第二行生成hex。这里的$LL是Keil内置宏展开后就是当前工程输出目录下不带扩展名的文件路径。如果你还想在生成后立即查看反汇编可以加一条fromelf.exe --text -c -o $LL.dis.txt $LL.axf这条命令会把axf中的指令逐条反汇编成文本调试时翻起来比在IDE里一屏一屏点方便很多。注意第一行命令里的--bin是“二进制输出”不是“输出到bin目录”别弄混了。另外Keil的Output页签里那个“Create HEX File”只生成hex不会生成bin想同时要bin就必须走fromelf。5. 烧录产线里的格式战争转换工具与交付选型5.1 Keil里同时生成hex和bin的完整配置如果你在工程目录里只看到hex和axf没有bin十有八九是没配置。完整做法分两步第一步在Options for Target - Output里勾选Create HEX File这样能得到hex第二步在Options for Target - User的After Build里加上fromelf生成bin的命令。注意勾选顺序无所谓但fromelf命令必须在编译成功之后才会执行。生成bin的同时还可以利用Keil的$L宏把bin输出到指定目录。比如我想把固件统一放到.\output目录下可以这样写fromelf.exe --bin -o output\$LL.bin $LL.axf反斜杠和路径分隔符在Windows命令行下要特别小心建议直接复制这段改一改。每次编译后你既可以用hex做调试器烧录也可以用bin做OTA或者产线烧录一套工程产出两种格式省得跟同事来回要文件。5.2 hex、bin、s19互转srec_cat实战srec_cat是个跨平台的命令行工具专门处理各种“带地址的烧录文件格式”。它对hex、bin、s19、elf都能读写而且支持合并、切割、填充、偏移等操作。我在Linux上处理固件时最常用它。# hex转bin srec_cat firmware.hex -intel -o firmware.bin -binary # bin转hex起始地址设为0x08000000 srec_cat firmware.bin -binary -o firmware.hex -intel -address-length4 -offset 0x08000000 # 合并Boot和App两个hex到一个文件 srec_cat boot.hex -intel app.hex -intel -o merged.hex -intel其中第二条命令里我加了-offset 0x08000000目的是让bin文件的第一个字节对应到0x08000000否则bin转hex会默认从地址0开始烧到MCU里就完全错位了。第三条命令在合并多个hex时很常用Bootloader固件和App固件想烧成一颗芯片的完整Flash镜像用这行命令就能生成一个合并后的hex。合并时如果两个hex的地址区间重叠srec_cat会报错或按参数决定覆盖策略所以先用-srec_cat的-Multiple和-Overwrite参数要谨慎我建议保持默认让重叠地址的冲突暴露出来再人工检查。srec_cat还有个高频用法是裁剪数据段。比如你只想提取hex里0x08010000到0x0801FFFF这一段给OTA升级可以这样srec_cat firmware.hex -intel -crop 0x08010000 0x08020000 -o app.bin -binary这条命令生成的就是从0x08010000开始、长度不超过0x10000的bin非常适合做分区升级包。5.3 不同厂商烧录工具对格式的偏好给你个参考表是我实际用过的工具和它们对格式的支持情况工具/设备支持格式使用注意STM32CubeProgrammerhex、bin、elf烧bin时要手动填起始地址J-Flashhex、bin、mot、elfbin需指定地址可加载axf需要转elfKeil MDKaxf、hex默认调试加载axf烧录用hexIAR EWARMaxf(elf)、hex、bin输出格式在Options里配openocdelf、hex、bin常见于GCC开发烧录命令直接给elf路径离线量产烧录器多为hex、bin、s19建议优先用bin起始地址防呆好做注意J-Flash这类通用工具虽然叫“Flash”但它默认是不认ARM的axf扩展名的要么先把axf用fromelf转成elf或hex要么在加载时把扩展名改成elf不过那样也不保险最好还是规范转换。而ST-Link的STM32CubeProgrammer则比较友好直接拖入axf也能解析因为它本质上是ELF。所以当你在几个工具之间切换时别默认“同一个文件哪里都能用”先确认格式。5.4 给产线的建议什么情况下必须用bin什么情况下用hex更省心最后从产线经验角度聊聊交付选型。如果你们厂里用的是离线烧录器一次烧成千上万片我强烈建议用bin并固定起始地址。原因很简单bin结构清晰工具加载快协议简单产线电脑上不易出错而hex是文本烧录器解析它要逐行算校验和效率略低一旦工具配置不对还可能把04记录解析错导致地址偏移。但如果固件里存在多个分散加载段或者你需要把一段数据烧到Flash、另一段初始化数据烧到RAM再启动那就必须用hex或s19。因为它们能精确表达“每段数据的起始地址和长度”bin做不到。此外很多汽车电子或功能安全项目要求固件文件带校验、带地址格式S19或HEX是行业默认这时候就不要试图把一切转成bin了。我个人实践下来还有一个建议给产线的bin文件最好在文件名或打包目录里明确标注起始地址比如app_0x08010000_v1.2.bin。不要指望每位产线同事都理解地址偏移一个清晰的文件名能避免烧错位置省下大量返工时间。真做到这一步hex、bin、axf这三个文件就不再是面试题而是你手里的工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑