LV020-从PC地址定位函数与源码行
LV005 打印出的现场里,PC 是定位出错指令的钥匙。本文用一次真实崩溃(PC = 0x08005D1C),把从地址反查函数与源码行的四条路线全部实测走一遍,并给出一条龙反查脚本。所有命令与输出均为实测,硬件平台 ATK-F103(STM32F103,Cortex-M3),工具链 Keil AC5.06。
一、 总览与准备工作
1. 四条路线总览
拿到 HardFault 打印里的 PC(如 0x08005D1C)后,按精度从高到低有四条路线:
| 路线 | 命令 / 操作 | 能定位到 | 依赖 |
|---|---|---|---|
| A. addr2line | arm-none-eabi-addr2line -e app.elf -f -C <addr> | 函数 + 文件 + 行号 | GNU 工具链 |
| A'. pyelftools | Python 脚本解析 DWARF | 函数 + 文件 + 行号 | pip install pyelftools |
| B. fromelf 反汇编 | fromelf -c app.axf > disasm.txt 再搜地址 | 函数 + 指令级上下文 | Keil 自带 |
| C. map 文件 | 搜 Global Symbols,找最近符号 | 函数级,数据地址会误判 | 构建产物 |
| D. μVision 调试器 | Disassembly 窗口输地址 | 函数 + 源码行(交互) | 调试器 |
2. 第 0 步:对齐固件与产物
这一步跳过,后面全白做。 反查用的 elf/map 必须就是产出这份 dump 的固件。对齐方法:
串口 banner: ATK-F103 USART1 ready, ... build Oct 15 2026 10:28:45
产物时间戳: atk-f103.map 2026/10/15 10:28:49
atk-f103.axf 2026/10/15 10:28:49banner 里的 __DATE__ __TIME__ 是编译时刻,axf/map 是链接时刻,后者晚几分钟属正常,日期时间数量级一致即可确认同一次构建。对不上就说明手上不是那份固件,去找对应的归档。
产物位置(Keil MDK 默认):
MDK-ARM/atk-f103/atk-f103.axf ; 链接产物, 带 DWARF 调试信息
MDK-ARM/atk-f103/atk-f103.map ; 链接.map, 含符号表
MDK-ARM/atk-f103/atk-f103.hex ; 烧录文件注意本工程的 build_flash.ps1 -c 会清理这些产物,重新构建即再生。
二、 四条反查路线实测
下面用同一次崩溃的五个地址(PC / LR / R1 / R2 / MSP)把四条路线各走一遍。
1. 路线 A:addr2line / pyelftools(到行号,首选)
1.1 把 axf 转成标准 ELF
Keil 的 .axf 本身是 ELF 且带 DWARF,GNU 工具多数情况能直接读;读不了就用 Keil 自带的 fromelf 转一份标准 ELF。fromelf 位于 C:\LenovoSoft\Keil_v5\ARM\ARMCC\Bin\:
cd E:\AI\bootloader\atk-f103\MDK-ARM\atk-f103
C:\LenovoSoft\Keil_v5\ARM\ARMCC\Bin\fromelf.exe --elf --output=app.elf atk-f103.axf实测:命令无回显,退出码 0,产物 app.elf 1,170,380 字节。
1.2 有 GNU 工具链:arm-none-eabi-addr2line
没有装 Arm GNU Toolchain 的,去 developer.arm.com 下「Arm GNU Toolchain」装上,bin 目录加进 PATH。然后:
arm-none-eabi-addr2line -e app.elf -f -C 0x08005D1C-e 指定 elf,-f 打印函数名,-C 符号解码。预期输出:
main
../Core/Src/main.c:74第 74 行就是 fault_test_illegal_addr() 里的 (void)*bad_addr;——测试函数被内联进 main,行号仍精确指向源码。批量反查多个地址:
arm-none-eabi-addr2line -e app.elf -f -C 0x08005D1C 0x080008ED 0x08004FCB1.3 没有 GNU 工具链:pyelftools 脚本
Windows + Keil 环境很可能没有 GNU 工具,用 Python 的 pyelftools(纯 Python 库)等效实现:
pip install pyelftools把下面脚本存为 elf_addr2line.py:
# -*- coding: utf-8 -*-
"""用 pyelftools 解析 Keil axf 转出的 elf, 反查地址 -> 函数/文件/行号
用法: python elf_addr2line.py app.elf 0x08005D1C [0x... ...]"""
import bisect
import sys
from elftools.elf.elffile import ELFFile
def build_tables(path):
"""返回 (lines, funcs): lines=[(addr,file,line)] 升序, funcs=[(addr,name)] 升序"""
lines, funcs = [], []
with open(path, "rb") as f:
elf = ELFFile(f)
if not elf.has_dwarf_info():
sys.exit("elf 无 DWARF 调试信息")
dw = elf.get_dwarf_info()
for CU in dw.iter_CUs():
lp = dw.line_program_for_CU(CU)
if lp is None:
continue
files = lp.header["file_entry"]
for e in lp.get_entries():
st = e.state
if st is None or st.end_sequence:
continue
idx = st.file - 1
name = files[idx].name if 0 <= idx < len(files) else b"?"
if isinstance(name, bytes):
name = name.decode()
lines.append((st.address, name, st.line))
for DIE in CU.iter_DIEs():
if DIE.tag != "DW_TAG_subprogram":
continue
low = DIE.attributes.get("DW_AT_low_pc")
if low is None:
continue
nm = DIE.attributes.get("DW_AT_name")
funcs.append((low.value, nm.value.decode() if nm else "?"))
lines.sort(key=lambda x: x[0])
funcs.sort(key=lambda x: x[0])
return lines, funcs
def lookup(lines, funcs, addr):
li = bisect.bisect_right([x[0] for x in lines], addr) - 1
fi = bisect.bisect_right([x[0] for x in funcs], addr) - 1
src = lines[li] if li >= 0 else None
fn = funcs[fi] if fi >= 0 else None
return fn, src
def main():
elf_path = sys.argv[1]
lines, funcs = build_tables(elf_path)
print("line entries: %d, functions: %d\n" % (len(lines), len(funcs)))
for a in sys.argv[2:]:
addr = int(a, 16)
fn, src = lookup(lines, funcs, addr)
fname = fn[1] if fn else "?"
foff = ("+0x%X" % (addr - fn[0])) if fn else ""
if src:
loc = "%s:%d" % (src[1], src[2])
soff = "(+0x%X)" % (addr - src[0])
else:
loc, soff = "?", ""
print("%s -> %s%s %s %s" % (a, fname, foff, loc, soff))
if __name__ == "__main__":
main()实测(对本次崩溃 dump 的五个地址):
python elf_addr2line.py app.elf 0x08005D1C 0x080008ED 0x08004FCB 0x08004FCC 0x20005DA0line entries: 11001, functions: 390
0x08005D1C -> main+0x28 ../Core/Src/main.c:74 (+0x0)
0x080008ED -> Default_Handler+0x6B3 startup_stm32f103xe.s:347 (+0x6A9)
0x08004FCB -> iap_tcp_init+0xBB iap_tcp.c:159 (+0x77)
0x08004FCC -> iap_tcp_is_connected+0x0 iap_tcp.c:207 (+0x0)
0x20005DA0 -> usart1_set_rx_sink+0x17FF6A64 ../Core/Src/usart.c:215 (+0x17FF6A60)elf 里共解析出 11001 条行号记录、390 个函数——调试信息确实在,这也是 1.2 节路线可行的证明。
1.4 实测结果怎么读
| dump 字段 | 值 | 反查结果 | 解读 |
|---|---|---|---|
PC | 0x08005D1C | main+0x28,../Core/Src/main.c:74 | ✅ 出错指令就是 main.c 第 74 行 (void)*bad_addr;,与测试函数设计完全一致 |
LR | 0x080008ED | Default_Handler+0x6B3,startup_stm32f103xe.s:347 | ⚠️ 误导归属,见下 |
R1 | 0x08004FCB | iap_tcp_init+0xBB,iap_tcp.c:159 | 该行是 IAP-TCP 的 printf 格式串,R1 指向串尾,printf 残留 |
R2 | 0x08004FCC | iap_tcp_is_connected+0x0,iap_tcp.c:207 | 下一函数首地址,同样是残留 |
MSP | 0x20005DA0 | 落到 usart1_set_rx_sink+0x17FF6A64 | ❌ 栈地址,不在代码行号表,输出无意义 |
三个必须知道的陷阱:
(1)只反查 Flash 代码地址(0x08xxxxxx)。MSP 是 RAM 地址、R0 = 0x30000000 是被访问的非法地址、R3/R12 = 0——这些值反查都会得到「最近的符号 + 一个荒谬的大偏移」,纯粹是噪声。所以第三章的一条龙脚本直接跳过非 Flash 地址。
(2)库函数没有调试信息,工具会给出误导归属。LR = 0x080008ED 被 DWARF 归到 Default_Handler,但反汇编(路线 B)证明它实际是 _printf_char_file 里 BL ferror 的返回地址——C 库是预编译的,无 DWARF,行号表的空隙被划给了相邻的有调试信息区域。行号工具的输出要用反汇编交叉验证,不能全信。
(3)偏移量差 1 是 Thumb 位。map 里 main 的 Value 是 0x08005CF5(奇数,含 Thumb 位),DWARF 给出的函数基址是偶数 0x08005CF4,所以 map 手算偏移是 +0x27,addr2line 给的是 +0x28——两者都对,只是基准差一个 bit0。
2. 路线 B:fromelf 反汇编(Keil 自带)
没有 GNU 工具、也没装 Python 库时,Keil 自带的 fromelf 足够完成函数级定位 + 指令级验证。
2.1 生成反汇编
C:\LenovoSoft\Keil_v5\ARM\ARMCC\Bin\fromelf.exe -c atk-f103.axf > axf_disasm.txt-c 输出带符号标签的完整反汇编。实测产物 axf_disasm.txt 约 3.08 MB(UTF-16 编码)。
2.2 搜地址
PowerShell 的 Select-String(findstr 对 UTF-16 支持不好,别用):
Select-String -Path axf_disasm.txt -Pattern "0x08005d1c:" -Context 2,2实测输出(节选):
0x08005d10: f001f9b2 .... BL param_init ; 0x8007078
0x08005d14: f7feffa2 .... BL iap_service_init ; 0x8004c5c
> 0x08005d1c: 6800 .h LDR r0,[r0,#0]
0x08005d1e: 4e0b .N LDR r6,[pc,#44]【注意】
上图为手工节选:命中行上方省略了 0x08005d18 处的 MOV.W r0,#0x30000000(4 字节指令,即 2.3 节引用的装载非法地址那条)。真实的 -Context 2,2 输出里,命中行的两条上文是 0x08005d14(BL)与 0x08005d18(MOV.W),而非图中的两条 BL。
2.3 怎么读结果
三条信息同时到手:
- 出错指令:
LDR r0,[r0,#0],往上两行是MOV r0,#0x30000000(反汇编里在0x08005d18),与 R0/BFAR 的0x30000000互印证; - 所在函数:反汇编按符号分段,从命中行往上翻到段首标签即是函数名(这里是
main); - 代码顺序:
BL iap_service_init之后紧跟测试代码,与源码「初始化全部完成才触发测试」的顺序一致。
1.4 节路线 A 里 LR 的误导归属,也是用这条路验证的——在 axf_disasm.txt 里搜 0x080008ed 的上下文,看到的是 _printf_char_file 里 BL ferror(0x080008e8)的下一行,实锤「LR 是最后一次 BL 的返回地址」。
3. 路线 C:map 文件(应急,函数级)
3.1 符号表在哪
打开 atk-f103.map,搜 Global Symbols 段,按地址升序列出全部代码符号:
Symbol Name Value Ov Type Size Object(Section)
hardfault_trampoline 0x0800020d Thumb Code 20 stm32f1xx_it.o(.emb_text)
_printf_char_file 0x080008d9 Thumb Code 32 _printf_char_file.o(.text)
HardFault_Handler 0x0800267f Thumb Code 6 stm32f1xx_it.o(i.HardFault_Handler)
hardfault_dump 0x080040e9 Thumb Code 1494 stm32f1xx_it.o(i.hardfault_dump)
iap_service_init 0x08004c5d Thumb Code 112 iap_service.o(i.iap_service_init)
iap_tcp_init 0x08004f11 Thumb Code 78 iap_tcp.o(i.iap_tcp_init)
iap_tcp_is_connected 0x08004fcd Thumb Code 12 iap_tcp.o(i.iap_tcp_is_connected)
main 0x08005cf5 Thumb Code 88 main.o(i.main)
mem_free 0x08005d55 Thumb Code 176 mem.o(i.mem_free)四列含义:Value 符号首地址(奇数,含 Thumb 位);Size 函数体字节数;末列所属目标文件。
3.2 查找方法:最近符号 + 区间校验
找 Value 不大于目标地址的最大符号,目标地址即落在该函数内。务必用 Size 列做区间校验:地址必须落在 [Value, Value+Size) 内,否则最近符号法失效(见 3.3.2)。把 dump 的五个地址逐个查:
| dump 字段 | 值 | map 反查 | 区间校验 |
|---|---|---|---|
PC | 0x08005D1C | main @ 0x08005CF5,+0x27 | ✅ main size 88,区间 0x08005CF5~0x08005D4D,命中 |
LR | 0x080008ED | _printf_char_file @ 0x080008D9,+0x14 | ✅ 在区间内,但不是调用链(见 3.3.1) |
R1 | 0x08004FCB | 最近符号 iap_tcp_init | ❌ 超出其 size 78 的范围,误判(见 3.3.2) |
R2 | 0x08004FCC | 最近符号 iap_tcp_init | ❌ 同上,实为下一函数首指令 |
MSP | 0x20005DA0 | 查不到 | 预期,map 只有代码符号 |
3.3 map 路线暴露的三个坑
3.3.1 LR 不是「出错函数的调用者」
教科书说栈帧 LR 槽是出错函数调用者的返回地址,但 -O3 下不成立。反汇编给出真相:
_printf_char_file
0x080008e0: BL _printf_char_common
0x080008e4: MOV r4,r0
0x080008e6: MOV r0,r5
0x080008e8: BL ferror
0x080008ec: b110 ... ← LR=0x080008ED 正是这条 BL 的返回地址0x080008ED 是 BL ferror 的返回地址。初始化链(iap_service_init → iap_tcp_init → printf → _printf_char_file)层层返回时没有指令恢复 LR,于是故障瞬间 LR 里留着整个程序最后一次执行的 BL 的返回地址。结论:LR 槽只说明「崩溃前最后一次函数调用发生在哪」,不说明调用链。可靠的只有 PC。
3.3.2 数据地址会被错归到最近代码符号
R1 = 0x08004FCB 落在函数间的 rodata——反汇编显示那是一串字符(IAP-TCP 打印格式串 "...listening on port %...\r\n")的结尾,R1 指向串尾 \0 的后一字节;R2 = 0x08004FCC 是下一函数 iap_tcp_is_connected 的首指令。两者都被「最近符号法」错归给 iap_tcp_init,因为该方法不看 Size。数据/常量地址必须靠区间校验或反汇编识别。
3.3.3 被内联的函数在 map 里没有符号
fault_test_illegal_addr 是 static 且只调用一次,被 -O3 内联——整个 map 搜不到这个符号。所以 PC 落在 main 不是定位失败,与内联事实互印证。注意 map 里 i. 前缀的条目(i.main、i.iap_tcp_init)是编译器的内联实例符号。
4. 路线 D:μVision 调试器
设备停在 HardFault_Handler 死循环时,调试器 attach(不要 reset,会丢现场):
(1)菜单 Debug → Start/Stop Debug Session(或用 attach 模式连已运行的目标)。
(2)View → Disassembly 打开反汇编窗口。
(3)窗口顶部地址栏输入 0x08005D1C 回车。
带调试信息的工程会显示源码行与汇编交织,函数名、行号一目了然。配合 View → Symbol Window 看符号地址。适合「行号找到了,还要看变量、外设、调用栈上下文」的场景——这是唯一能继续往下查(比如看谁改写了指针)的路线。
三、 一条龙反查:从串口日志直接到源码行
日常排查不要手动复制地址。把下面脚本存为 dump_resolve.py,串口日志整段喂给它,一次输出所有寄存器的归属:
#!/usr/bin/env python3
"""HardFault dump -> 源码行 一条龙
依赖: pip install pyelftools
用法: python dump_resolve.py app.elf < dump.txt"""
import bisect
import re
import sys
from elftools.elf.elffile import ELFFile
FLASH_LO, FLASH_HI = 0x08000000, 0x08080000 # 本板 F103ZE 512KB 内部 Flash, 之外的地址不反查
def build_tables(path):
lines, funcs = [], []
with open(path, "rb") as f:
elf = ELFFile(f)
if not elf.has_dwarf_info():
sys.exit("elf 无 DWARF 调试信息(检查 Debug 选项是否被关掉)")
dw = elf.get_dwarf_info()
for CU in dw.iter_CUs():
lp = dw.line_program_for_CU(CU)
if lp is None:
continue
files = lp.header["file_entry"]
for e in lp.get_entries():
st = e.state
if st is None or st.end_sequence:
continue
idx = st.file - 1
name = files[idx].name if 0 <= idx < len(files) else b"?"
lines.append((st.address, name, st.line))
for DIE in CU.iter_DIEs():
if DIE.tag != "DW_TAG_subprogram":
continue
low = DIE.attributes.get("DW_AT_low_pc")
if low is None:
continue
nm = DIE.attributes.get("DW_AT_name")
funcs.append((low.value, nm.value.decode() if nm else "?"))
lines.sort(key=lambda x: x[0])
funcs.sort(key=lambda x: x[0])
return lines, funcs
def fmt(tables, addr):
lines, funcs = tables
la = [x[0] for x in lines]
fa = [x[0] for x in funcs]
li = bisect.bisect_right(la, addr) - 1
fi = bisect.bisect_right(fa, addr) - 1
parts = []
if fi >= 0:
parts.append("%s+0x%X" % (funcs[fi][1], addr - funcs[fi][0]))
if li >= 0:
name = lines[li][1]
name = name.decode() if isinstance(name, bytes) else name
parts.append("%s:%d" % (name, lines[li][2]))
return " ".join(parts) if parts else "(无调试信息覆盖)"
def main():
if len(sys.argv) < 2:
sys.exit("usage: python dump_resolve.py app.elf < dump.txt")
tables = build_tables(sys.argv[1])
text = sys.stdin.read()
print("==== dump 地址反查 ====")
for field in ("R0", "R1", "R2", "R3", "R12", "LR", "PC"):
m = re.search(r"^%s\s*=\s*0x([0-9A-Fa-f]+)" % field, text, re.M)
if not m:
continue
addr = int(m.group(1), 16)
tag = " <-- 出错指令" if field == "PC" else ""
if not (FLASH_LO <= addr < FLASH_HI):
print("%-4s 0x%08X -> (非 Flash 代码地址, 不反查)%s" % (field, addr, tag))
continue
print("%-4s 0x%08X -> %s%s" % (field, addr, fmt(tables, addr), tag))
m = re.search(r"frame offset = (-?\d+)", text)
if m:
print("frame offset = %s" % m.group(1))
if __name__ == "__main__":
main()实测(PowerShell 用管道,因为不支持 < 重定向):
Get-Content dump.txt | python dump_resolve.py app.elf==== dump 地址反查 ====
R0 0x30000000 -> (非 Flash 代码地址, 不反查)
R1 0x08004FCB -> iap_tcp_init+0xBB iap_tcp.c:159
R2 0x08004FCC -> iap_tcp_is_connected+0x0 iap_tcp.c:207
R3 0x00000000 -> (非 Flash 代码地址, 不反查)
R12 0x00000000 -> (非 Flash 代码地址, 不反查)
LR 0x080008ED -> Default_Handler+0x6B3 startup_stm32f103xe.s:347
PC 0x08005D1C -> main+0x28 ../Core/Src/main.c:74 <-- 出错指令
frame offset = -4一眼看到重点:PC 在 main.c:74,出错指令实锤;LR 那行的 Default_Handler 是库函数无调试信息造成的误导(1.4 节陷阱(2)),别被带偏。
四、 产物归档清单
上述方法全部依赖构建产物,发布时必须归档:
| 文件 | 用途 | 不归档的后果 |
|---|---|---|
.axf / .elf | addr2line、pyelftools、μVision | 无法得到行号 |
.map | 应急函数级定位 | 连函数名都没有 |
.hex / .bin | 复现问题 | 无法确认设备上跑的是哪版 |
| 版本号 + 构建时间 | 与串口 banner 对齐 | 不知道分析对象是哪份固件 |
配套习惯:固件 banner 打印 __DATE__ __TIME__(LV005 的做法);Debug 选项保持生成调试信息(DWARF 只影响 elf 体积,不进 Flash);知道固件的优化等级(-O3 下内联、LR 过期值、rodata 混排三个坑都在线,-O0 下解读结论不同)。
本文档由 markdowncli 技能辅助生成