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 | |||
.map | |||
.elf | |||
.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 | arm-none-eabi-gcc -c main.c -o main.o | |
gcc -S | arm-none-eabi-gcc -S main.c -o main.s | |
-Wa,-ahlms= | arm-none-eabi-gcc -c -Wa,-ahlms=main.lst main.c | |
-Wl,-Map= | arm-none-eabi-gcc ... -Wl,-Map=firmware.map -o firmware.elf | |
objcopy -O binary | arm-none-eabi-objcopy -O binary firmware.elf firmware.bin | |
objcopy -O ihex | 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 | 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 地址,怎么定位到源码
把上面的工具串起来,走一遍最常用的流程:
抓现场:HardFault 处理函数里把 PC、LR 存下来,串口打印。比如 PC = 0x08001234,LR = 0x08000abc。 PC 直接定位: addr2line -e firmware.elf -f -C 0x08001234,得到崩溃的函数和行号。LR 追溯调用者: addr2line解析 LR。注意 LR 指向的是"调用点之后的那条指令",真正的调用语句在它前面一行。没符号或地址不在代码区:查 .map看这个地址落在哪个段;如果不在 .text 里,多半是栈溢出、返回地址被写坏、函数指针飞了。看汇编确认: 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 的。















































































