Skip to content

LV020-从PC地址定位函数与源码行 ​

LV005 打印出的现场里,PC 是定位出错指令的钥匙。本文用一次真实崩溃(PC = 0x08005D1C),把从地址反查函数与源码行的四条路线全部实测走一遍,并给出一条龙反查脚本。所有命令与输出均为实测,硬件平台 ATK-F103(STM32F103,Cortex-M3),工具链 Keil AC5.06。

一、 总览与准备工作 ​

1. 四条路线总览 ​

拿到 HardFault 打印里的 PC(如 0x08005D1C)后,按精度从高到低有四条路线:

路线命令 / 操作能定位到依赖
A. addr2linearm-none-eabi-addr2line -e app.elf -f -C <addr>函数 + 文件 + 行号GNU 工具链
A'. pyelftoolsPython 脚本解析 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 的固件。对齐方法:

text
串口 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:49

banner 里的 __DATE__ __TIME__ 是编译时刻,axf/map 是链接时刻,后者晚几分钟属正常,日期时间数量级一致即可确认同一次构建。对不上就说明手上不是那份固件,去找对应的归档。

产物位置(Keil MDK 默认):

text
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\:

powershell
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。然后:

bash
arm-none-eabi-addr2line -e app.elf -f -C 0x08005D1C

-e 指定 elf,-f 打印函数名,-C 符号解码。预期输出:

text
main
../Core/Src/main.c:74

第 74 行就是 fault_test_illegal_addr() 里的 (void)*bad_addr;——测试函数被内联进 main,行号仍精确指向源码。批量反查多个地址:

bash
arm-none-eabi-addr2line -e app.elf -f -C 0x08005D1C 0x080008ED 0x08004FCB

1.3 没有 GNU 工具链:pyelftools 脚本 ​

Windows + Keil 环境很可能没有 GNU 工具,用 Python 的 pyelftools(纯 Python 库)等效实现:

powershell
pip install pyelftools

把下面脚本存为 elf_addr2line.py:

python
# -*- 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 的五个地址):

powershell
python elf_addr2line.py app.elf 0x08005D1C 0x080008ED 0x08004FCB 0x08004FCC 0x20005DA0
text
line 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 字段值反查结果解读
PC0x08005D1Cmain+0x28,../Core/Src/main.c:74✅ 出错指令就是 main.c 第 74 行 (void)*bad_addr;,与测试函数设计完全一致
LR0x080008EDDefault_Handler+0x6B3,startup_stm32f103xe.s:347⚠️ 误导归属,见下
R10x08004FCBiap_tcp_init+0xBB,iap_tcp.c:159该行是 IAP-TCP 的 printf 格式串,R1 指向串尾,printf 残留
R20x08004FCCiap_tcp_is_connected+0x0,iap_tcp.c:207下一函数首地址,同样是残留
MSP0x20005DA0落到 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 生成反汇编 ​

powershell
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 支持不好,别用):

powershell
Select-String -Path axf_disasm.txt -Pattern "0x08005d1c:" -Context 2,2

实测输出(节选):

text
  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 段,按地址升序列出全部代码符号:

text
    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 反查区间校验
PC0x08005D1Cmain @ 0x08005CF5,+0x27✅ main size 88,区间 0x08005CF5~0x08005D4D,命中
LR0x080008ED_printf_char_file @ 0x080008D9,+0x14✅ 在区间内,但不是调用链(见 3.3.1)
R10x08004FCB最近符号 iap_tcp_init❌ 超出其 size 78 的范围,误判(见 3.3.2)
R20x08004FCC最近符号 iap_tcp_init❌ 同上,实为下一函数首指令
MSP0x20005DA0查不到预期,map 只有代码符号

3.3 map 路线暴露的三个坑 ​

3.3.1 LR 不是「出错函数的调用者」 ​

教科书说栈帧 LR 槽是出错函数调用者的返回地址,但 -O3 下不成立。反汇编给出真相:

armasm
_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,串口日志整段喂给它,一次输出所有寄存器的归属:

python
#!/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 用管道,因为不支持 < 重定向):

powershell
Get-Content dump.txt | python dump_resolve.py app.elf
text
==== 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 / .elfaddr2line、pyelftools、μVision无法得到行号
.map应急函数级定位连函数名都没有
.hex / .bin复现问题无法确认设备上跑的是哪版
版本号 + 构建时间与串口 banner 对齐不知道分析对象是哪份固件

配套习惯:固件 banner 打印 __DATE__ __TIME__(LV005 的做法);Debug 选项保持生成调试信息(DWARF 只影响 elf 体积,不进 Flash);知道固件的优化等级(-O3 下内联、LR 过期值、rodata 混排三个坑都在线,-O0 下解读结论不同)。


本文档由 markdowncli 技能辅助生成