夜雨聆风学习资料网

ARTICLE · 1344388

一个崩溃地址,怎么定位到源码?

一个崩溃地址,怎么定位到源码?

程序跑飞了,串口打印 HardFault,PC = 0x08001234。你盯着这个地址,心里犯嘀咕:这到底是哪一行代码崩的?

再或者,编译完发现生成了 firmware.bin 和 firmware.elf 两个文件,烧进板子的是 bin,但调试器(J-Link、GDB)加载的却是 elf。为什么不能直接烧 elf?lst、map 又是干嘛的?

这些问题都指向同一件事:一个 .c 文件变成能烧进板子的程序,中间要经历编译、汇编、链接、转换好几步,每一步都会吐出不同的产物。这些产物——bin、elf、lst、map——各有各的用途,搞不清它们,调 bug 时就像蒙着眼睛摸路。这篇把它们一次讲清,再配上常用的工具指令,最后走一遍"一个地址定位到源码"的完整流程。

一、一个 C 文件到能烧录,中间发生了什么

从源码到烧录,大致是四步:

.c 源文件  │  (1) 编译 compile          arm-none-eabi-gcc -c  ▼.o 目标文件(机器码 + 符号,还没分配地址)  │  (2) 链接 link              arm-none-eabi-ld / gcc  ▼.elf 可执行文件(地址定好、符号齐全、带调试信息)  │  (3) 转换 convert           arm-none-eabi-objcopy  ▼.bin / .hex 烧录镜像(纯机器码)

这里有两个关键点:

  • .lst
     是编译期的副产品:编译时顺带把"源码每一行对应哪段汇编"记下来,相当于施工日志。
  • .map
     是链接期的副产品:链接时顺带把"每个函数、每个变量放在哪个地址"记下来,相当于一张城市地图。

二、四种文件,各是干什么的

文件
谁生成的
里面有什么
主要给谁用
.lst
编译器
源码 ↔ 汇编逐行对照
人看,理解编译器把 C 翻成了什么
.map
链接器
符号地址、段(.text/.data/.bss)分布
人看,根据地址反查函数/变量
.elf
链接器
机器码 + 段信息 + 符号表 + 调试信息
调试器(J-Link/GDB)
.bin
转换工具
纯机器码,无地址无校验
烧录器

一个一个说:

.bin:最朴素,只有机器码

bin 就是纯二进制机器码,从头到尾是连续的 0 和 1,没有地址信息、没有校验。所以烧录时你得另外告诉烧录器"烧到哪个起始地址"。它最干净,适合整片从头烧。

.elf:机器码之外,还带"说明书"

ELF(Executable and Linkable Format)是链接器生成的完整可执行文件。它除了机器码,还带了:

  • 段信息:代码段 .text、只读数据 .rodata、已初始化全局变量 .data、未初始化全局变量 .bss 分别放哪、加载到哪个地址;
  • 符号表:每个函数名、全局变量名对应哪个地址;
  • 调试信息(DWARF):哪段机器码对应源码第几行。

这就是为什么调试器要加载 elf——它能告诉调试器"0x08001234 是 foo() 函数、在 foo.c 第 235 行"。烧录用 bin 是因为芯片启动阶段没有 ELF Loader,只能吃纯二进制;而 bin 就是把 elf 里这些"说明书"全剥掉之后剩下的机器码。

.lst:施工日志

lst(list)把"每一行 C 源码"和"它编译成的汇编/机器码"并排打印。看它最有用的时候,是你不确定某句 C 到底被编译成了几条指令、循环有没有被优化掉。

.map:城市地图

map 记录链接后的最终布局:每个函数、每个全局变量的地址,.text/.data/.bss 各占多大、从哪里到哪里。程序跑飞时,拿到一个地址,就能在 map 里查到"它落在哪个函数/变量范围里"。

一个必看的数字:map 文件末尾会统计各段大小。判断程序多大,记住两句话——

  • 占 Flash 的大小 ≈ Code + RO Data + RW Data
  • 运行时占 RAM 的大小 ≈ RW Data + ZI Data(ZI 就是 bss,未初始化、上电清零的)

注意:hex 文件在电脑上的磁盘大小 ≠ 实际占 Flash 的大小,别拿文件大小当程序大小,要看 map 里的统计。

三、编译 vs 汇编 vs 链接

这三个词经常混着说,其实是三步:

  • 编译(compile):把 C 翻译成汇编语言。gcc -S main.c 就能看到这一步的产物 .s。
  • 汇编(assemble):把汇编语言翻译成机器码,生成目标文件 .o。这一步是机械的一一对应。
  • 链接(link):把多个 .o 和库拼起来,给每个符号分配最终地址,生成 .elf。

一句话:编译管"翻译",汇编管"转码",链接管"分配地址、把大家拼一起"。

("汇编"这个词有点歧义:它既指"汇编语言",也指"把汇编翻译成机器码"这一步。看上下文就能区分。)

四、反汇编 vs 反编译

两个"反",方向都是把机器码往回翻译,但目标不同:

  • 反汇编(disassembly):机器码 → 汇编指令。因为机器码和汇编是一一对应的,所以反汇编是精确、无损的。objdump -d 干的就是这个。
  • 反编译(decompilation):机器码 → 高级语言(C)。这一步是"猜",因为 C 到机器码丢了很多信息(变量名、类型、结构全没了),反编译只能近似还原逻辑,可读性一般,但理解一段没有源码的固件逻辑时很有用(工具如 Ghidra、IDA)。

记住区别:反汇编忠实还原"汇编",反编译是近似还原"C"。嵌入式里定位崩溃,绝大多数时候反汇编 + 符号表就够了,轮不到反编译。

五、常用工具指令

下面以 ARM Cortex-M 常用的 arm-none-eabi-* 工具链为例(RISC-V 换成 riscv64-unknown-elf-*,命令名一样)。重要:所有工具必须和你编译固件用的是同一套工具链、同一份 elf,否则地址对不上。

命令
干什么
示例
gcc -c
C 编译成 .o
arm-none-eabi-gcc -c main.c -o main.o
gcc -S
只看汇编
arm-none-eabi-gcc -S main.c -o main.s
-Wa,-ahlms=
生成 .lst
arm-none-eabi-gcc -c -Wa,-ahlms=main.lst main.c
-Wl,-Map=
生成 .map
arm-none-eabi-gcc ... -Wl,-Map=firmware.map -o firmware.elf
objcopy -O binary
elf 转 bin
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
objcopy -O ihex
elf 转 hex
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
objdump -d
反汇编
arm-none-eabi-objdump -d firmware.elf
(带源码用 -S)
nm
列符号表
arm-none-eabi-nm -n -S firmware.elf
size
看各段大小
arm-none-eabi-size firmware.elf
readelf
看 elf 结构
arm-none-eabi-readelf -S firmware.elf
(-h 看头)
addr2line
地址→函数+行号
arm-none-eabi-addr2line -e firmware.elf -f -C 0x08001234

其中addr2line是定位崩溃的"主角":给它一个地址,它直接吐出来这个地址属于哪个函数、源码第几行:

$ arm-none-eabi-addr2line -e firmware.elf -f -C 0x08001234wifi_rx_process/path/src/wifi.c:235

-f 打印函数名,-C 把 C++ 符号还原成可读的名字。如果得到 ??:?,说明这个地址不在符号范围里(可能是栈溢出、函数指针乱跳、返回地址被冲了),那就不是普通崩溃。

六、一个 HardFault 地址,怎么定位到源码

把上面的工具串起来,走一遍最常用的流程:

  1. 抓现场:HardFault 处理函数里把 PC、LR 存下来,串口打印。比如 PC = 0x08001234,LR = 0x08000abc。
  2. PC 直接定位:addr2line -e firmware.elf -f -C 0x08001234,得到崩溃的函数和行号。
  3. LR 追溯调用者:addr2line 解析 LR。注意 LR 指向的是"调用点之后的那条指令",真正的调用语句在它前面一行。
  4. 没符号或地址不在代码区:查 .map 看这个地址落在哪个段;如果不在 .text 里,多半是栈溢出、返回地址被写坏、函数指针飞了。
  5. 看汇编确认:objdump -d --start-address=0x08001200 --stop-address=0x08001250 firmware.elf,看崩溃点附近的指令,确认是不是指针非法、除以 0、对齐错误。

关键提醒:崩溃的位置,不等于 bug 的位置。最常见的是数组越界把内存写坏了,程序跑了好一会儿,才在某个无辜的 memcpy() 里 HardFault。所以定位到崩溃点后,还要结合寄存器(R0、R1 是不是野指针)往前查谁写了坏数据。

最后一句实用的

四个文件一句话记:lst 看翻译,map 看布局,elf 给调试器,bin 给烧录器。

定位崩溃的口诀:PC 用 addr2line,LR 追调用链,地址飞了查 map,汇编看 objdump。记住——所有这些工具,都必须用和你编译固件同一次、同一套工具链生成的那份 elf,否则地址对不上,全是白忙。

相关阅读

  • 《上拉、下拉、开漏、推挽:GPIO 四个模式一次分清》
  • 《网口调试绕不开的基础常识:IPv4、IPv6、PoE、交换机和子网掩码》

「启动流程与内存分布」方向后续还会展开:代码段/数据段/BSS/堆/栈分别放哪、程序是怎么跳到 main 的。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料