4. 调试符号与可执行文件加载

当硬件连接建立、TRACE32 能够访问目标芯片之后,调试环境还只是"看得见内存",却"读不懂程序"。此时在 TRACE32 中单步执行,往往只能看到一片反汇编和寄存器数值,无法定位到 C 源码行,也无法按函数名下断点。造成这一现象的根本原因,是调试器缺少两样东西:可执行文件中的机器码与地址映射,以及调试符号中的源码行号、变量类型与函数信息。本节要解决的就是把这两样东西正确加载进 TRACE32,并设置好初始 PC 与栈指针,使源码与机器指令形成一一对应的映射关系。

ELF 与 DWARF:一个文件里的两类信息

嵌入式工程编译后通常产出 ELF(Executable and Linkable Format)文件,例如 firmware.elf。ELF 是一个容器,内部按"节"(section)组织数据,与调试最相关的几类节如下:

  • .text:机器指令,即真正烧写到 Flash 的代码;
  • .data:已初始化的全局/静态变量,运行时位于 RAM,初值存放在 Flash;
  • .bss:未初始化或初值为零的全局/静态变量,只占 RAM 空间,不占 Flash;
  • .rodata:只读常量、字符串字面量;
  • .symtab / .strtab:符号表与字符串表,提供函数名、全局变量名及其地址;
  • .debug_*:DWARF 调试信息节,包括 .debug_info、.debug_line、.debug_abbrev、.debug_str 等。

DWARF 是嵌入在 ELF 中的调试信息格式,它描述的不是"程序怎么运行",而是"程序在源码层面长什么样"。其中 .debug_info 用 DIE(Debugging Information Entry)树描述编译单元、函数、变量、类型;.debug_line 则是一张行号表,把每条机器指令的地址映射回源文件与行号。TRACE32 正是依靠这张表,才能在你单步时把 PC 值翻译成"main.c 第 42 行"。

因此,编译时必须让工具链生成调试信息。以 GCC 为例,编译与链接阶段都要带 -g,并建议使用 -gdwarf-4 或更高版本以兼容较新的 TRACE32:

arm-none-eabi-gcc -c -g -gdwarf-4 -O0 -mcpu=cortex-m4 \
    -o main.o main.c
arm-none-eabi-gcc -g -gdwarf-4 -T link.ld \
    -o firmware.elf main.o startup.o
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
注意:-O0 或 -Og 下源码与指令的对应关系最清晰;若使用 -O2,变量可能被优化进寄存器甚至消除,单步时会出现"跳行"或变量显示 <optimized out>,这属于正常现象而非 TRACE32 故障。

加载可执行文件与调试符号

在 TRACE32 中加载 ELF 使用 Data.LOAD.Elf 命令。它同时完成两件事:把各节的初值按地址写入目标内存,并读取 DWARF 符号建立源码映射。典型命令如下:

Data.LOAD.Elf firmware.elf /NoClear
Data.LIST

其中 /NoClear 表示加载前不清除目标内存,适合在已有运行环境上追加符号;若希望干净加载,可省略该选项。执行后可用 Data.LIST 查看已加载的节及其地址范围,确认 .text 落在 Flash 区、.data 落在 RAM 区。

如果只想加载符号而不改写内存(例如程序已由烧写器写入 Flash),应使用:

Data.LOAD.Elf firmware.elf /NoCode

反之,若只想下载代码而暂不加载符号,可用 /NoSymbol。实际工程中更常见的做法是:先用烧写工具把 firmware.bin 写入 Flash,再用 /NoCode 只加载符号,避免重复擦写。

设置初始 PC 与栈指针

符号加载完成后,TRACE32 知道了"代码在哪、符号叫什么",但还不知道"从哪里开始执行"。对于 Cortex-M 内核,复位后硬件会从向量表读取初始 MSP 和复位向量,因此通常无需手工设置;但对于通过调试器直接启动、或从 RAM 中运行的场景,就需要显式设置 PC 与 SP:

Register.Set PC Reset_Handler
Register.Set SP 0x20020000
Register.Set MSP 0x20020000

这里 Reset_Handler 是启动文件中的复位入口符号,0x20020000 是栈顶地址(示例值,需按实际链接脚本中的 RAM 末端确定)。设置完成后,用 Register.View 或 Register 窗口确认 PC 指向 Reset_Handler 的第一条指令,SP 指向 RAM 高地址端。

栈指针必须 8 字节对齐(Cortex-M 的 AAPCS 要求),且应指向 RAM 有效范围的高端。若 SP 落在非法地址,第一次压栈就会触发 HardFault,表现为"刚启动就死机"。

验证映射是否正确

判断符号与源码映射是否成功,最直接的方法是打开 List 窗口(源码与反汇编混合视图)。若左侧显示 C 源码行、右侧显示对应机器指令,且地址连续,说明 DWARF 行号表已被正确解析。此时在 main 函数处下断点:

Break.Set main /Program
Go

程序应停在 main 的第一行源码处。若断点无法命中,常见原因有三:一是 ELF 与烧写到目标上的二进制不一致(重新编译后未重新烧写);二是加载时使用了 /NoCode 但目标 Flash 中并无对应代码;三是链接脚本中的地址与 TRACE32 的地址映射配置不符。可先用 Data.LOAD.Elf firmware.elf /NoCode 重新加载符号,再用 List 核对地址是否与目标内存一致。

ELF 文件 → TRACE32 调试视图 映射链路 firmware.elf .text 机器指令 Flash 0x08000000 .data 已初始化变量 RAM 0x20000000 .bss 未初始化变量 RAM 仅占空间 .symtab 符号表 函数名 / 变量名 DWARF 调试信息 .debug_info 类型/函数 .debug_line 行号表 .debug_abbrev 缩写 Data.LOAD.Elf TRACE32 调试器 符号解析 函数名 → 地址 行号映射 地址 → 源文件:行号 变量类型解析 结构体 / 数组 / 枚举 初始 PC / SP 设置 Reset_Handler / 0x20020000 地址映射校验 List 窗口核对 断点命中验证 调试视图 List 混合视图 C 源码 + 反汇编 Var 变量窗口 按类型显示值 Register 寄存器 PC / SP / MSP Break 断点 按函数名/行号 源码级单步执行 编译带 -g -gdwarf-4 → Data.LOAD.Elf → 设置 PC/SP → List 窗口验证映射

常见问题与排查要点

实际工程中,符号加载失败或映射错乱通常集中在几个环节。第一,编译时只对部分文件加了 -g,导致某些函数能显示源码、另一些只能看反汇编,此时应检查整个工程的编译选项是否统一。第二,ELF 与目标上的二进制版本不一致,表现为断点地址与 List 显示地址对不上,解决方法是重新烧写或重新加载符号。第三,链接脚本中 .text 的起始地址与 TRACE32 的地址映射配置不一致,例如芯片有 Flash 重映射或地址别名,需要在 TRACE32 的 SYStem 配置中正确设置 SYStem.Option 或 MAP 命令。第四,DWARF 版本过新,旧版 TRACE32 无法解析,可改用 -gdwarf-4 重新编译。

把上述环节串起来看,本节的核心逻辑是:ELF 提供机器码与地址,DWARF 提供源码与类型的对应关系,TRACE32 通过 Data.LOAD.Elf 把两者读入并建立映射,再通过设置 PC 与 SP 让程序从正确位置开始执行。只有这条链路完整,后续的断点、单步、变量观察、性能分析才有意义。建议在每次重新编译后,都养成"重新加载 ELF → 核对 List 窗口 → 在 main 下断点验证"的习惯,用最小成本确认调试环境始终与目标代码保持一致。