ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Keil MDK 从编译链接到 hex 生成:fromelf 配置与排查

Keil MDK 从编译链接到 hex 生成:fromelf 配置与排查 1. 从 C 源码到 hex一次编译到底走了几步1.1 四个阶段各自干了什么很多人用 Keil 用了好几年点的是那个“Rebuild”按钮看到的是 Build Output 里滚过的一屏屏文字但真要问一句“hex 是哪一步产生的”答不上来的不在少数。搞清楚这条链路后面所有关于“hex 没生成”“hex 是旧的”“烧进去不跑”的问题都能自己定位。Keil MDK 在 ARM 架构下的编译链路大致是这样源文件先经过编译器AC5 时代的armccAC6 时代的armclang翻译成目标文件.o这些.o再和启动文件、库文件一起交给链接器armlink链接器的产物是.axf这是一个标准的 ELF 可执行文件带完整符号表和调试信息最后才是格式转换由fromelf这个工具把.axf里的可加载段抽出来按 Intel HEX 或者纯二进制的规则重写成.hex或.bin。注意这里的顺序关系没有链接成功就一定不会有 hex。我见过有人反复勾选“Create HEX File”却不知道工程里有一个L6218E未定义符号的链接错误链接器根本没跑到最后一步那个勾选项自然什么都不做。所以排查 hex 缺失的第一反应应该是往 Build Output 窗口最下面翻看有没有一行0 Error(s), 0 Warning(s)。顺带说一句 Keil C518051 平台的差异它的编译器是C51链接器是BL51或LX51最后生成 hex 的是OH51。工具链名称不同但“编译—链接—格式转换”三段式结构是完全一致的理解了一个另一个自然通。1.2 hex、bin、axf 到底有什么区别这三个文件经常被混着叫但在产线和量产环节混用会出大问题我把它们的差异列清楚。文件类型本质是否含地址信息是否含调试符号常见用途.axfELF 可执行文件有段表描述有DWARF 调试信息Keil 在线调试、下载器识别.hexIntel HEX 文本有每条记录带地址无产线烧录、量产工具、OTA 分包.bin纯二进制镜像无靠烧录地址约定无固定起始地址的批量烧录、合并固件.axf是“原稿”体积最大含调试信息Keil 点击 Download 或进入 Debug 时调试器读的就是它。.hex是文本格式每一行都自带地址所以烧录工具不关心你从哪个地址开始写它自己会解析。.bin最“裸”纯粹是字节流烧录时必须手动指定起始地址一旦填错轻则跑不起来重则把 Bootloader 覆盖掉。理解这一点就能解释一个常见现象为什么 Keil 里点 Download 能正常烧写把 hex 拖进第三方烧录工具却报地址错误因为下载器认的是.axf的段信息而某些第三方工具在没有正确配置 Flash 起始地址时会默认从0x00000000开始写跟芯片实际的 Flash 基址对不上。1.3 为什么你的 Objects 目录里就是没有 hex这是新手问得最多的一个问题我按出现频率从高到低排个序第一Options for Target → Output里的Create HEX File没勾。这个是新手最常踩的勾选之后重新编译一次即可注意是重新编译而不是只保存设置。第二工程编译没通过。链接阶段有错误armlink提前退出fromelf根本没被调用。第三你改过输出目录。Output 选项卡里有一个Select Folder for Objects按钮如果指向了别的路径hex 就生成在那里而不是默认的Objects\。我遇到过同事因为这个找了一个下午最后发现文件安静地躺在..\..\build\output\里面。第四链接器的输出名被改过。Options for Target → Linker里Name of Executable可以自定义hex 的文件名默认跟目标名Target Name一致而不是跟你以为的工程名一致。一个工程里多个 Target输出名通常各不相同。第五杀毒软件或企业安全策略拦截了fromelf.exe的写盘动作。这种情况比较少见但确实存在表现是编译日志正常、文件就是不出现临时关闭实时防护试一次就能确认。2. 生成 hex 的开关在哪工程配置逐项确认2.1 Output 选项卡里的 Create HEX File打开工程后右键左侧 Target 名称选Options for Target xxx切到Output页这是核心位置。需要关注三处Select Folder for Objects决定.o、.axf、.hex落在哪个目录。我一般会把它显式指向工程根目录下的Objects避免默认路径跟随工程位置乱跳。Name of Executable.axf和.hex的主文件名来源。留空就用 Target 名。Create HEX File这个复选框就是“生成 hex”的总开关。勾上以后每次成功的 Build 或 Rebuild 都会顺带生成一份 hex。它下面还有Create Batch File、Browse Information等选项跟 hex 无关。注意Create HEX File只在“链接成功”的前提下生效。如果你看到 Build Output 里写着.\Objects\demo.axf - 1 Error(s), 0 Warning(s).那这一轮的 hex 是不会被刷新的——目录里如果还有旧的 hex那是上一次的产物千万别拿去烧。另外提醒一句AC6 工具链下有些老工程的配置是从 AC5 迁移过来的迁移后 Output 页不会自动帮你勾选需要手动确认。这类工程经常出现“明明以前能生成 hex换电脑就不行了”的怪现象其实就是配置文件里这个位丢了。2.2 器件包、内核与启动文件必须匹配生成 hex 的前提是链接能把所有段安排到合法地址上而地址的合法性来自器件信息。Keil 从 MDK5 开始用 Pack 机制管理器件Options for Target → Device页里选的芯片型号决定了 IROM1 和 IRAM1 的默认起始地址与大小。举个例子STM32F103C8T6 的 Flash 是 64KB起始0x08000000如果你在 Device 页选了一个 128KB 的型号Keil 会按大容量去分配地址链接照样能过hex 也照样生成但烧到小容量芯片上超过 64KB 的那部分数据直接写不进去或者被丢弃跑起来就是随机崩溃。这类问题在换型号、换批次的时候特别容易出。Pack 包的安装位置也值得一提。MDK5 默认把 Pack 放在C:\Users\用户名\AppData\Local\Arm\Packs一旦系统盘空间紧张或者公司做镜像时清了用户目录Device页里的器件列表就会空掉一大片。这时候可以装到别的盘通过环境变量把 Pack 根目录指过去再重新打开 Keil 让 Pack Installer 重新索引。装不上的时候先看 Pack Installer 右下角有没有Updating卡住再去看网络和磁盘权限别一上来就重装整个 Keil。启动文件同样关键。startup_stm32f103xb.s这类文件负责建立中断向量表和初始栈指针它里面的Stack_Size、Heap_Size和向量表长度必须跟芯片型号对应。用错启动文件的典型症状是编译通过、hex 生成、烧录也成功但一上电就 HardFault。这种坑跟 hex 生成机制无关但常常被误以为是“hex 坏了”值得提前说清楚。2.3 链接器与分散加载对 hex 内容的影响链接阶段决定了 hex 里“有哪些数据、从哪个地址开始”。默认情况下 Keil 会用一个自动生成的分散加载文件把代码放 IROM1、数据放 IRAM1。但真实项目经常会改自定义分散加载Scatter File比如做 Bootloader App 双区方案App 的代码要从0x08004000开始就必须写一个.sct文件在Options for Target → Linker页取消Use Memory Layout from Target Dialog指定自定义文件。多段布局有些芯片支持把常量数据放到外挂的 QSPI Flash 区域链接器会把它们安排成独立的段最终体现在 hex 里就是地址跳跃的记录。EEPROM 或配置区如果要用链接器生成一段固定地址的配置数据需要在 scatter 文件里显式定义一个0的段否则数据可能被排到别处。自定义分散加载最容易出的问题是地址冲突链接器报L6226E: Missing section或者L6407E: Sections of aggregate size ...。这时候先别改代码把.map文件打开看一眼链接器生成的 map 文件在Objects\*.map里面详细列出了每个段落在哪个地址、占多少字节对照芯片手册一看就清楚。提示改过 scatter 文件之后务必做一次Rebuild all target files。增量编译有时会复用上一次链接的结果导致你改了地址却看不出变化。3. 完整实操一次干净编译并产出 hex3.1 编译前的清场动作我个人的习惯是只要动过工程配置器件、优化等级、头文件路径、分散加载第一步一定是Project → Clean Targets然后Rebuild。Clean 会删掉Objects目录下所有中间产物Rebuild 则强制全量重编避免增量编译带来的陈旧依赖问题。清场还有个实际好处能确认产物是不是真的在预期目录生成。如果 Clean 之后Objects文件夹里还有东西说明输出目录被改到别处了这时候顺手看一眼 Output 页的路径设置比事后到处找文件高效得多。编译之前顺手检查一下Options for Target → C/C里的Include Paths。头文件路径写错是最常见的编译错误之一报错形式是error: #5: cannot open source input file xxx.h。Keil 的路径支持相对路径用..\..\Drivers\STM32F1xx_HAL_Driver\Inc这种写法比绝对路径更靠谱换电脑、换盘符也不会失效。3.2 执行编译并读懂 Build Output按下F7Build或Project → Rebuild all target filesBuild Output 窗口会滚出一屏日志。日志的结尾几行最关键典型的成功输出长这样Build started: Project: demo *** Using Compiler V5.06 update 7 (build 960), folder: C:\Keil_v5\ARM\ARMCC\Bin Build target demo compiling main.c... compiling stm32f1xx_hal_gpio.c... linking... Program Size: Code12480 RO-data464 RW-data32 ZI-data1584 FromELF: creating hex file... .\Objects\demo.axf - 0 Error(s), 0 Warning(s). Build Time Elapsed: 00:00:09重点看三处Program Size告诉你资源占用情况Code 是代码段字节数RO-data 是只读常量RW-data 是已初始化的可写数据ZI-data 是零初始化数据其中 RW-data ZI-data 大致对应 RAM 消耗FromELF: creating hex file...这一行出现说明 hex 正在生成这是最直接的证据最后0 Error(s), 0 Warning(s)是判定依据。Program Size这行很容易被忽略但它在容量接近上限时非常有用。比如 STM32F103C8T6 只有 64KB Flash 和 20KB RAM当 Code RO-data RW-data 逼近 65536 字节时链接器会直接报L6406E: No space in execution regions这时候需要开优化等级、裁减不必要的库或者换更大容量的芯片。3.3 验证 hex 文件是否可用生成之后不要急着烧先做三件事。第一看文件时间戳。打开Objects目录确认.hex的修改时间就是刚刚这是最省事的“有没有刷新”判断。第二看文件大小和首尾内容。用记事本或者 VS Code 打开 hex第一行应该是一条数据或扩展地址记录最后一行必然是:00000001FF这条叫 EOF 记录所有合法的 Intel HEX 都以它结尾。如果最后一行不是它文件一定被截断过。第三做一次实际烧录验证。Keil 自带下载功能走的是.axf想验证 hex 本身是否完好可以用厂商的独立烧录工具比如 STM32CubeProgrammer、J-Flash 之类打开 hex 文件看它解析出来的地址范围和大小是否符合预期没有报校验失败就基本可以。注意用第三方工具烧录 hex 时务必确认工具里的起始地址设置与 hex 内部记录的地址并不冲突。Intel HEX 自带地址工具通常会自动识别但如果手动勾了“从 0x08000000 开始”而文件里已经有扩展线性地址记录个别老工具会重复计算导致数据错位。4. 读懂 hex 文件本身的格式4.1 Intel HEX 的记录结构与校验和算法能读 hex 的人排查烧录问题会快一大截。Intel HEX 是纯文本每行一条记录格式固定:LLAAAATT[DD...]CC拆开看冒号是行首标记LL是这一行数据字节数一字节十六进制AAAA是 16 位地址TT是记录类型DD是若干数据字节CC是校验和。记录类型一共六种常用的是这四种类型名称含义00数据记录真正的固件数据01文件结束文件末尾标记无数据02扩展段地址段基址乘以 16 后加到后续地址上04扩展线性地址高 16 位地址STM32 工程最常用校验和的算法很简单把LL、AAAA、TT、所有DD相加取低 8 位再用0x100减去这个值结果就是CC。取反加一的本质就是把所有字节求和后凑成 0。拿一条真实记录验证一下:10010000214601360121470136007EFE09D2190140LL0x10表示 16 字节数据AAAA0x0100TT00是数据记录。把这行的所有字节相加0x100x010x000x000x210x460x010x360x010x210x470x010x360x000x7E0xFE0x090xD20x190x01 0x3C0取低 8 位得0xC00x100 - 0xC0 0x40与行尾的40完全一致。以后遇到文件被怀疑损坏随便挑几行手工验算一下就能判断。再比如 STM32 工程里几乎必然出现的一行:020000040800F2LL02AAAA0000TT04数据是08 00表示后续所有数据的基地址是0x08000000这正是 STM32 的 Flash 起始地址。校验和验算0x020x000x000x040x080x00 0x0E0x100 - 0x0E 0xF2对上了。最后是结束记录:00000001FF长度 0、地址 0、类型 01、无数据校验和0x100 - 0x01 0xFF。看到这一行才说明文件是完整写下的。4.2 STM32 工程生成的 hex 长什么样把 Keil 生成的 hex 打开结构大致是开头一条或几条类型 04 记录设定基地址接着是大量类型 00 的数据记录地址从0x08000000开始连续增长。中间如果遇到不连续的区域比如某个段被安排到了0x08008000你会看到新的类型 04 记录出现把高 16 位地址切换过去。有一个细节值得注意Keil 默认生成的 hex只包含有实际数据的区域中间的空洞不会用 0xFF 填充。所以文件大小通常比芯片实际容量小得多64KB 的 Flash 编出来 12KB 的 hex 是完全正常的。但有些烧录工具在写入时会自动把空洞填0xFF再整片擦写这个行为差异会导致烧录时间明显不同别误以为是工具“卡住了”。如果工程里有 Bootloader 和 App 两个 Target各自生成的 hex 地址范围通常是分开的。合并烧录的时候需要把两个 hex 拼成一个手动拼接是不现实的用srec_catSRecord 工具集这类工具可以一条命令完成合并。也可以先各自转成 bin再用二进制拼接的方式按偏移量合并但要注意 bin 没有地址信息偏移量必须自己算准。4.3 hex、bin 之间的转换与合并Keil 本身只能直接生成 hex想要 bin 就得借助fromelf。这个工具在 Keil 安装目录下的ARM\ARMCC\bin或者ARM\ARMCLANG\bin里命令行用法很直接fromelf --bin --outputObjects\demo.bin Objects\demo.axf fromelf --i32 --outputObjects\demo.hex Objects\demo.axf fromelf --text -c -o Objects\demo.dis Objects\demo.axf第一条生成纯二进制第二条生成 Intel HEX--i32就是 Intel 32 位 hex 的意思第三条生成反汇编清单调 HardFault 的时候特别有用。想让 Keil 在每次编译后自动跑 fromelf可以在Options for Target → User页的After Build/Rebuild里加一条命令C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output $LL.bin #L这里的$LL.bin和#L是 Keil 的关键序列分别代表“把链接输出文件名的扩展名替换为 bin”和“链接输出文件的完整路径”。不同版本的 Keil 对这些序列的支持略有差别如果发现命令没生效先把路径写死试通再回头换变量别在变量语法上耗时间。5. 编译报错、hex 不生成、编译很慢的排查手册5.1 高频报错速查表我把这几年踩过和帮人解决过的错误整理成表遇到报错先对号入座。报错信息大概率原因处理方式cannot open source input file xxx.h头文件路径未添加C/C 页 Include Paths 补齐对应目录L6218E: Undefined symbol xxx源文件或库未加入工程把对应.c加入 Target或补上库依赖L6406E: No space in execution regionsFlash 或 RAM 超限提高优化等级、裁剪库或调整分区L6226E: Missing sectionscatter 文件段定义与代码不匹配对照.map检查段名和地址identifier xxx is undefined头文件没包含或拼写错检查 include 与大小写Flash Download failed - Could not load fileaxf 不存在或路径变了重新编译检查 Output 路径Error: L6915E库与工程配置冲突检查 MicroLIB 与标准库设置#1-D: last line of file ends without a newline文件末尾无换行加一个空行属于警告不影响产物还要提醒一个新手常犯的误解警告Warning不等于可以忽略。像#177-D: variable x was declared but never referenced这类确实无所谓但像#1295-D: Deprecated declaration、隐式类型转换导致的#188-D在开启高优化等级时可能直接变成逻辑错误。一个干净的工程应该在0 Warning状态下构建别让警告越积越多。5.2 编译成功却没有 hex 的排查路径如果 Build Output 明明是0 Error(s)但 hex 就是不见按这个顺序排查确认Create HEX File勾选状态。改完配置要点 OK不能只关窗口。确认 Target 选对了。多 Target 工程里Build 的是当前激活的那个而Create HEX File是每个 Target 独立配置的。切换 Target 后重新勾一次。搜索整个工程目录找 hexdir /s /b *.hex或者在资源管理器里按修改时间排序。往往它就在你没想到的地方。看FromELF那行日志有没有出现。如果日志里完全没有FromELF字样说明勾选没生效如果出现了但报错错误信息会紧跟在后面。检查Options for Target → Linker里Name of Executable是否被改成带路径的奇怪名字。排除杀毒软件干扰临时关闭实时防护再编译一次。我印象最深的一次是同事的工程路径里有中文和空格fromelf在调用时路径没被正确引用导致写文件失败。把工程挪到纯英文、无空格的短路径下问题立刻消失。这类“路径玄学”在 Windows 上的老旧工具链里并不罕见工程路径短、纯英文、层级浅是个好习惯。5.3 编译慢的真实原因逐条排查“Keil5 编译很慢”是热词里出现频率很高的抱怨原因其实就那几条逐条排除很快。第一Browse Information 开着。Options for Target → Output里的Browse Information会生成符号浏览数据库方便跳转定义代价是编译时间显著增加。不写代码的时候关掉能省下不少时间。第二每次都 Rebuild。Rebuild 是全量重编Build 是增量。改一个.c文件用 Build 就够了只有改头文件、改配置、改 scatter 的时候才需要 Rebuild。第三优化等级为-O0。调试阶段用-O0是合理的但发布构建应该切到-O1或-O2编译时间反而不一定更长因为优化器会消掉大量冗余代码。注意切优化等级后要重新验证功能volatile用漏的地方在-O2下容易暴露。第四头文件包含路径过多。Include Paths 里挂了几十个目录编译器每处理一个#include都要在这些目录里挨个查开销是累加的。该删的删掉也是工程卫生的一部分。第五杀毒软件实时扫描。编译过程中会产生成百上千个中间文件杀软逐个扫描会拖慢整条流水线。把工程目录和 Keil 安装目录加入白名单提速效果立竿见影。第六硬件和路径。工程放在机械硬盘、网络映射盘或者 U 盘上编译速度会明显下降。这种事听起来像玄学但换成固态硬盘本地盘之后几万行代码的工程从四十秒掉到十几秒是真实存在的。第七宏定义与条件编译滥用。大量#ifdef嵌套会让预处理和编译都变慢而且可读性差。用配置结构体或者编译期常量替代一部分条件编译代码和编译速度都会受益。6. 进阶命令行编译与产物管理6.1 用 UV4 做无人值守编译图形界面点着编译适合开发阶段但做持续集成、批量出包的时候命令行更省事。Keil 提供了UV4.exe的命令行模式C:\Keil_v5\UV4\UV4.exe -j0 -b D:\proj\demo.uvprojx -o D:\proj\build.log-b表示构建增量-r表示重建-c表示清理-j0表示隐藏主窗口-o指定日志输出文件。执行完之后日志文件里会有跟图形界面一样的构建结果直接判断0 Error(s), 0 Warning(s)即可。返回值方面大致规律是 0 表示无错误无警告1 表示有警告2 表示有错误其它非零值都可能对应不同的失败类型。不同版本的具体编码有细微差别做自动化判断时建议以日志文本为准返回值作辅助避免版本升级后流水线莫名失败。这套命令行方式还有个好处它不需要人工确认弹窗。图形界面在编译时会弹各种提示自动化脚本没法处理命令行模式天然避开了这个问题。不过要注意命令行编译依然受 License 约束如果工程里用了需要额外授权的中间件可能会在无人值守时失败这种时候日志里会有明确的 License 相关错误别当作普通编译错误去查。6.2 fromelf 生成 bin 与反汇编fromelf除了生成 hex 和 bin还有个容易被忽视的用法——反汇编fromelf --text -c -o Objects\demo.dis Objects\demo.axf生成的.dis文件里每条机器指令前面都带着对应的 C 源码行号程序跑飞、HardFault 定位的时候把故障寄存器的 PC 值往这个文件里一查立刻知道当时执行到哪一行。这比反复加打印语句高效得多。还可以用--bincombined把多个加载段合并成一个连续的 bin适合有多个不连续段的工程。另外--vhx、--i32这些输出格式参数各有用途具体可以在命令行敲fromelf --help看完整列表不同版本支持度不一样。提示反汇编文件里的地址是文件内的虚拟地址跟 Flash 里的物理地址可能有偏移。看故障地址时先确认链接脚本里的加载地址再对号入座不然会查到一个完全无关的函数上。6.3 产物归档与版本管理习惯固件产物.axf、.hex、.bin、.map、.dis通常不应该提交到代码仓库体积大、且每次编译都变。但发布版本必须归档我的做法是中间产物Objects目录加入.gitignore保持仓库干净。每次正式发版把 hex、bin、map 三个文件按项目名_版本号_日期的规则重命名单独存到一个发布目录或者制品库。.map文件一定要留。它记录了每个符号的地址和大小出问题时没有 map 基本等于盲查。记录下编译时的工具链版本和优化等级。同一份代码在不同版本编译器下生成的 hex 可能不同线上出问题时这个信息能帮你复现环境。最后提一个小技巧。产线上经常需要合并 Bootloader 与 App 的固件用srec_cat或者厂商自带的合并工具把两个 hex 按地址合并成一个再用统一地址烧录能省掉产线切换固件文件的动作减少人为失误。合并之后建议随机抽几台做回读校验确认合并后的地址分布与预期一致这个检查只需要一次但能避免整批返工。我在实际项目里踩过最深的一个坑是把增量编译出来的旧 hex 交给了产线——因为改动了头文件里的一个宏编译器没有重新编译所有依赖该头文件的源文件产物看起来是新的行为却是旧的。从那以后凡是涉及发布一律Clean加Rebuild all宁可多等两分钟也不在版本上冒任何风险。
返回列表