Keil 编译文件 .axf 内容深度分析
1. 引言
在嵌入式开发中,Keil MDK(Microcontroller Development Kit)编译链接后生成的.axf文件(ARM Executable Format)是最终的可执行映像文件。它本质上是一个容器,不仅包含烧录到芯片中的程序代码和初始化数据,还携带了丰富的调试信息、符号表、重定位信息、段属性等元数据。
理解.axf文件的结构,对于以下场景至关重要:
- 内存布局分析:确认代码、数据、堆栈在 Flash 和 RAM 中的分布,判断是否超出芯片容量。
- 运行时异常定位:当程序进入 HardFault 时,通过 PC 值反查符号和源码行号。
- 代码体积优化:按函数统计代码大小,找出体积异常的模块。
- 链接问题排查:检查符号是否重复定义、未解析的外部引用等。
- 深入理解编译器行为:通过反汇编查看编译器优化后的实际指令序列。
本文将系统介绍.axf文件的内部结构,并演示如何使用fromelf、arm-none-eabi-objdump等工具解析其内容,配合大量实操示例帮助读者快速上手。
2. .axf 文件本质
2.1 ELF 格式基础
.axf文件本质上是ELF(Executable and Linkable Format)格式的一种变体,严格遵循 ARM 的 ELF 规范(ARM ELF)。它与 Linux 下 GCC 生成的.elf文件在结构上完全一致,区别仅在于可能包含 ARM 特有的段(Section)和调试信息格式。
ELF 文件从逻辑上分为三个层次:
第一层:ELF 头(ELF Header)
位于文件最开头,固定长度(32 位架构下为 52 字节,64 位为 64 字节)。它定义了:
- 文件标识魔数:
0x7FELF(4 字节) - 文件类别:32 位(
ELFCLASS32)或 64 位(ELFCLASS64) - 字节序:小端(
ELFDATA2LSB)或大端(ELFDATA2MSB) - 目标架构:ARM(
EM_ARM = 40) - 文件类型:可执行文件(
ET_EXEC) - 入口点地址:程序第一条指令的虚拟地址
- 程序头表和节头表在文件中的偏移和大小
第二层:程序头表(Program Header Table)
描述操作系统加载器如何将文件映射到内存。对于嵌入式裸机程序,程序头表通常只包含一个PT_LOAD段,描述整个映像的加载地址和大小。
第三层:节头表(Section Header Table)
描述文件中各个段的详细信息,是分析.axf最常用的入口。
2.2 核心段详解
一个典型的.axf文件包含以下核心段:
| 段名 | 类型 | 存储位置 | 内容 |
|---|---|---|---|
.text | 代码段 | Flash | 程序指令、函数体 |
.rodata | 只读数据段 | Flash | 字符串常量、const变量、查表数据 |
.data | 读写数据段 | 初始在 Flash,启动时复制到 RAM | 已初始化的全局/静态变量 |
.bss | 零初始化段 | RAM | 未初始化的全局/静态变量 |
.heap | 堆区 | RAM | malloc动态分配区域 |
.stack | 栈区 | RAM | 函数调用栈、局部变量 |
.debug_* | 调试信息段 | 可选(可剥离) | DWARF 格式的源码级调试信息 |
2.3 链接脚本与段布局
段的最终地址由链接脚本(Linker Script,.sct或.ld文件)决定。典型的 STM32 链接脚本会定义:
LR_IROM1 0x08000000 0x00100000 { ; 加载域:Flash 起始 0x08000000,大小 1MB ER_IROM1 0x08000000 0x00100000 { ; 执行域 *.o (RESET, +First) ; 中断向量表放在最前面 *.o (InRoot$$Sections) .ANY (+RO) ; 所有只读段(.text + .rodata) } RW_IRAM1 0x20000000 0x00020000 { ; RAM 域 .ANY (+RW +ZI) ; 读写段 + 零初始化段 } }理解链接脚本是准确分析.axf内存布局的前提。
3. 常用分析工具
3.1 fromelf(Keil 自带)
fromelf.exe是 Keil MDK 自带的 ELF 文件分析工具,位于ARM\ARMCC\bin\(ARM Compiler 5)或ARM\ARMCLANG\bin\(ARM Compiler 6)目录下。它功能强大,是分析.axf的首选工具。
常用命令详解:
# 1. 查看 ELF 头信息fromelf--elfoutput.axf# 输出示例:# ** ELF Header **# Class: ELF32# Data: 2's complement, little endian# Entry point: 0x08000500# Flags: 0x00000002, Version5# 2. 查看所有段信息(最常用)fromelf--sectionoutput.axf# 输出包含:段名、大小、起始地址、类型、属性# 3. 反汇编代码段(输出到文件)fromelf--text-coutput.axf>disasm.txt# --text 表示只输出代码段,-c 表示反汇编# 4. 导出符号表fromelf--symbolsoutput.axf# 输出所有符号的名称、地址、大小、类型# 5. 生成内存映射报告(比 .map 文件更详细)fromelf--mapoutput.axf>mem_map.txt# 包含每个目标文件对每个段的贡献# 6. 导出调试信息fromelf--debugoutput.axf>debug_info.txt# 7. 查看重定位信息fromelf--relocsoutput.axf# 8. 查看未解析符号fromelf--unresolvedoutput.axf3.2 arm-none-eabi-objdump(GNU 工具链)
如果使用 ARM GCC 工具链,可以用 GNU Binutils 系列命令:
# 查看 ELF 头arm-none-eabi-readelf-hfirmware.axf# 查看所有段(含大小和地址)arm-none-eabi-objdump-hfirmware.axf# 查看段详细信息(含内容)arm-none-eabi-readelf-Sfirmware.axf# 反汇编(混合源码模式)arm-none-eabi-objdump-S-dfirmware.axf>disasm.txt# -S 表示尽可能显示源码行,-d 表示反汇编# 查看符号表arm-none-eabi-nm firmware.axf|sort# 或更详细:arm-none-eabi-readelf-sfirmware.axf# 查看重定位表arm-none-eabi-readelf-rfirmware.axf# 地址转源码行号arm-none-eabi-addr2line-efirmware.axf 0x08000632-f-C# -f 显示函数名,-C 还原 C++ 名字修饰3.3 十六进制查看器
对于底层分析,可以用HxD(Windows)、010 Editor(支持 ELF 模板)、xxd(Linux)等工具直接打开.axf文件,对照 ELF 规范解析二进制结构。
使用010 Editor配合 ELF 模板,可以直观地查看:
- ELF 头的每个字段值
- 节头表中每个段的详细信息
- 符号表中每个符号的绑定和类型
这对于理解 ELF 格式本身非常有帮助。
4. 核心分析内容
4.1 内存布局分析
通过fromelf --section可以查看每个段的起始地址、大小和属性,这是分析内存布局最直接的方法。
完整输出示例:
Section Size Address Type ----------------- ---------- ------------- -------- RESET 0x00000400 0x08000000 Data .text 0x00002A34 0x08000400 Code .rodata 0x00000120 0x08002E34 Data .data 0x00000080 0x20000000 Data .bss 0x00000400 0x20000080 Zero .heap 0x00000200 0x20000480 Zero .stack 0x00000400 0x20000680 Zero关键计算:
- Flash 总占用= RESET(0x400) + .text(0x2A34) + .rodata(0x120) =0x2F54 字节(约 12KB)
- RAM 总占用= .data(0x80) + .bss(0x400) + .heap(0x200) + .stack(0x400) =0x1080 字节(约 4KB)
- 代码密度= 代码段大小 / 总 Flash 占用 = 0x2A34 / 0x2F54 ≈ 89%
注意事项:
.data段在 Flash 中也有备份(加载时地址),用于启动时复制到 RAM,但fromelf --section通常只显示运行时地址。.bss段在 Flash 中不占空间,仅在 RAM 中占用。- 堆和栈的大小由链接脚本或启动文件中的符号定义,实际使用量需运行时分析。
实操:判断芯片选型是否合理
假设芯片为 STM32F103C8T6(Flash 64KB,RAM 20KB),从上面数据看 Flash 占用仅 12KB,RAM 仅 4KB,选型完全满足且有较大余量。如果 Flash 占用接近 60KB,则需要考虑升级芯片或优化代码体积。
4.2 符号表分析
符号表是.axf文件中最重要的元数据之一,它记录了每个符号(函数、全局变量、静态变量)的名称、地址、大小和类型。
查看所有符号:
fromelf--symbolsoutput.axf|head-30输出示例:
0x08000500 0x000000A4 Func main 0x080005A4 0x0000002C Func HAL_Init 0x080005D0 0x00000038 Func HAL_TIM_Base_Init 0x08000608 0x0000001C Func HAL_TIM_PWM_Init 0x08000624 0x0000000E Func HAL_GPIO_WritePin 0x20000000 0x00000004 Data uwTick 0x20000004 0x00000004 Data uwMillis 0x20000008 0x00000200 Data uartBuffer符号类型说明:
| 类型 | 含义 | 示例 |
|---|---|---|
Func | 函数 | main,HAL_Init |
Data | 已初始化数据 | uwTick,uartBuffer |
Zero | 零初始化数据 | .bss中的变量 |
Thumb | Thumb 代码 | 某些内联函数 |
Label | 标签 | 中断向量表中的地址 |
实用技巧:
- 查找特定函数地址(用于设置断点或分析调用栈):
fromelf--symbolsoutput.axf|grep"HAL_UART_Transmit"- 按大小排序找出最大函数(代码体积优化):
fromelf--symbolsoutput.axf|grep"Func"|sort-t'x'-k2-rn|head-10- 检查变量是否被意外优化掉:
如果代码中定义了某个全局变量,但在符号表中找不到,说明编译器可能将其优化掉了(例如定义了但未使用)。可以在变量定义处加volatile防止优化。
- 分析链接顺序:
符号表中的地址顺序反映了链接器处理目标文件的顺序。如果发现某个模块的函数地址异常靠后,可能是链接脚本中段排序导致的。
4.3 反汇编分析
反汇编是将机器指令还原为汇编代码的过程,是深入理解编译器生成代码的最终手段。
生成反汇编文件:
fromelf--text-coutput.axf>disasm.txt反汇编文件结构:
main main.sct: 10 0x08000500: b500 PUSH {lr} main.sct: 11 0x08000502: 4806 LDR r0,[pc,#24] ; @0x0800051C main.sct: 12 0x08000504: f000f80a BL HAL_Init main.sct: 13 0x08000508: 2000 MOVS r0,#0x00 main.sct: 14 0x0800050A: bd00 POP {pc}每列含义:
| 列 | 内容 | 说明 |
|---|---|---|
| 1 | 0x08000500 | 指令地址(Flash 中的绝对地址) |
| 2 | b500 | 机器码(16 位 Thumb 指令) |
| 3 | PUSH {lr} | 反汇编后的汇编指令 |
常见 ARM Thumb 指令解读:
| 指令 | 含义 | 用途 |
|---|---|---|
PUSH {lr} | 将链接寄存器压栈 | 函数开头保存返回地址 |
POP {pc} | 出栈到 PC | 函数返回 |
BL label | 带链接的跳转 | 调用函数 |
LDR r0,[pc,#imm] | 从 PC 相对地址加载 | 加载常量或全局变量地址 |
MOVS r0,#0 | 立即数移动 | 设置返回值 |
STR r0,[r1] | 存储寄存器到内存 | 写变量 |
LDR r0,[r1] | 从内存加载到寄存器 | 读变量 |
反汇编实战:分析函数调用开销
假设我们有以下 C 代码:
voiddelay_us(uint32_tus){for(uint32_ti=0;i<us*10;i++){__NOP();}}反汇编后可能看到:
0x08000600: b510 PUSH {r4, lr} 0x08000602: 4604 MOV r4, r0 0x08000604: eb00 0040 ADD r0, r0, r0, LSL #16 ; us * 10 的优化计算 0x08000608: 1e40 SUBS r0, r0, #1 0x0800060A: d1fd BNE 0x08000608 0x0800060C: bd10 POP {r4, pc}可以看到编译器将乘法优化为移位加法,循环体仅 3 条指令(SUBS + BNE + NOP 内联),效率很高。
4.4 调试信息提取
.axf文件包含 DWARF(Debugging With Attributed Record Formats)调试信息,这是实现源码级调试的基础。
提取调试信息:
fromelf--debugoutput.axf>debug_info.txtDWARF 信息包含的内容:
- 行号表(Line Number Table):将指令地址映射到源码文件和行号
0x08000500 main.c:10 0x08000502 main.c:11 0x08000504 main.c:12- 类型信息(Type Information):变量和函数参数的类型定义
<1><0x123>: DW_TAG_structure_type DW_AT_name: (const char*) "GPIO_TypeDef" DW_AT_byte_size: 0x400 <2><0x12F>: DW_TAG_member DW_AT_name: (const char*) "CRL" DW_AT_type: <0x200> DW_AT_data_member_location: 0- 变量位置信息:局部变量在栈上的偏移或寄存器位置
<1><0x456>: DW_TAG_variable DW_AT_name: (const char*) "temp" DW_AT_type: <0x300> DW_AT_location: DW_OP_fbreg -8调试信息的实际用途:
- 调试器源码级调试:Keil Debugger、J-Link、OpenOCD 都依赖 DWARF 信息实现单步执行和变量查看。
- 代码覆盖率分析:通过行号表可以统计哪些源码行被执行过。
- 性能分析:结合行号表和定时器中断,可以统计每个函数的执行时间。
注意:调试信息会显著增大.axf文件体积(有时比代码本身大 5-10 倍)。发布 Release 版本时,可以使用fromelf剥离调试信息:
fromelf--stripdebug output.axf-ooutput_stripped.axf5. 实战案例:分析 HardFault
HardFault 是嵌入式开发中最常见的运行时异常之一。当程序执行了非法操作(如访问空指针、除零、执行未对齐访问等),CPU 会触发 HardFault 异常。通过.axf文件,我们可以快速定位问题根源。
5.1 场景描述
假设程序运行后进入 HardFault,调试器报告 PC 值为0x08000632。我们需要找出这个地址对应哪段代码。
5.2 定位方法
方法 1:用 fromelf 查找地址对应的符号
fromelf--symbolsoutput.axf|grep0x08000632输出:
0x08000630 0x0000001C Func UART_Process说明 PC 位于UART_Process函数内部(地址范围 0x08000630 ~ 0x0800064B)。
方法 2:反汇编后搜索地址
fromelf--text-coutput.axf|grep-A10"0x08000632"输出:
0x08000630: 6800 LDR r0,[r0,#0x00] 0x08000632: 6001 STR r1,[r0,#0x00] ; <-- HardFault 发生处 0x08000634: 4770 BX lr可以看到0x08000632是一条STR r1,[r0,#0x00]指令,功能是将 r1 的值写入 r0 指向的内存地址。如果 r0 为 0(空指针),则触发 HardFault。
方法 3:用 addr2line 工具(GNU 工具链)
arm-none-eabi-addr2line-eoutput.axf 0x08000632-f-C输出:
UART_Process uart.c:42说明 HardFault 发生在uart.c第 42 行,对应函数UART_Process。
5.3 根因分析
结合反汇编和源码,我们查看uart.c:42:
// uart.c 第 40-45 行voidUART_Process(UART_HandleTypeDef*huart){if(huart->Instance==USART1){// 第 41 行huart->Instance->DR=data;// 第 42 行 <-- HardFault}}问题在于huart指针为 NULL,访问huart->Instance时触发空指针访问。
5.4 更多 HardFault 常见原因
| 原因 | 典型指令 | 排查方法 |
|---|---|---|
| 空指针访问 | LDR r0,[r0,#0]或STR r1,[r0,#0] | 检查调用者传入的指针是否为空 |
| 数组越界 | LDR r0,[r1,r2,LSL #2] | 检查数组索引是否超出范围 |
| 除零 | SDIV r0,r0,r1且 r1=0 | 检查除数是否为零 |
| 栈溢出 | PUSH {lr}时 SP 超出栈区 | 检查栈大小是否足够,是否有无限递归 |
| 未对齐访问 | LDR r0,[r1,#3]且 r1 未对齐 | 检查指针是否对齐到 4 字节边界 |
| 访问外设未使能时钟 | STR r1,[r0,#0]到外设地址 | 检查对应外设时钟是否已使能 |
5.5 完整排查流程
- 记录异常现场:从调试器或 Fault Handler 中获取 PC、LR、SP 值。
- 定位 PC 对应函数:使用
fromelf --symbols或addr2line。 - 反汇编查看指令:分析导致异常的指令类型。
- 检查寄存器值:通过调试器查看异常发生时的寄存器状态(特别是 R0-R3)。
- 回溯调用栈:通过 LR 寄存器值,使用相同方法逐层回溯调用链。
- 修复代码:根据分析结果修复 Bug。
6. 高级技巧
6.1 分析代码体积
按函数统计代码体积:
fromelf--symbolsoutput.axf|grep"Func"|awk'{print $3, $5}'|sort-rn|head-206.2 检查未使用的函数
通过fromelf --refs查看引用关系,找出未被调用的函数:
fromelf--refsoutput.axf|grep"UNUSED"6.3 生成内存映射文件
fromelf --map生成的映射文件比 Keil IDE 的.map文件更详细,包含每个目标文件的贡献:
fromelf--mapoutput.axf>mem_map.txt7. 总结
.axf文件是嵌入式开发中最重要的中间产物之一。掌握其分析方法,能够帮助开发者:
- 快速定位运行时异常
- 精确控制内存布局
- 优化代码体积和执行效率
- 深入理解编译器行为
建议在日常开发中养成分析.axf文件的习惯,尤其是在遇到链接错误、内存溢出、HardFault 等问题时,.axf文件往往能提供最直接的线索。
