3. 嵌入式Linux内核调试符号与镜像准备
用TRACE32调试嵌入式Linux内核,第一步不是连上JTAG,而是把“符号世界”和“运行世界”对齐。内核调试的本质是:调试器通过符号表把内存地址翻译成函数名、变量名和源码行号,再把断点、观察点、栈回溯落到正确的指令位置。如果符号与镜像不匹配,或者地址随机化没有处理,TRACE32看到的函数名和实际运行的代码就会错位,断点可能永远不命中,栈回溯也会指向错误位置。本节围绕vmlinux、System.map、内核模块符号、KASLR处理以及调试信息裁剪与恢复展开。
vmlinux:内核调试的符号根基
vmlinux是编译内核后生成的ELF文件,包含完整的符号表、调试信息(DWARF)、节区布局和代码段。TRACE32加载vmlinux后,才能按函数名设置断点、查看全局变量、解析调用栈。需要注意,vmlinux本身通常不是最终烧录到目标板的镜像,真正运行的是经过压缩或格式转换的Image、zImage或uImage。因此调试时存在“符号来自vmlinux、代码来自运行镜像”的对应关系,二者必须来自同一次编译。
在TRACE32中加载符号的典型命令如下:
Data.LOAD.ELF vmlinux /NoCode /NoClear
; 仅加载符号与调试信息,不写入目标内存
Data.LOAD.ELF vmlinux 0xffff800000000000
; 若已知内核加载基址,可指定偏移加载
第一条命令只把符号信息读入调试数据库,适合内核已经由Bootloader加载完成、只需调试的场景;第二条在需要把符号按实际运行地址重定位时使用。判断符号是否加载成功,可以执行sYmbol.List查看符号列表,或用List *start_kernel*确认关键函数是否存在。
System.map:地址与符号的对照表
System.map是内核编译时生成的纯文本符号地址表,每行格式为“地址 类型 符号名”。它不包含调试信息,但体积小、可读性强,适合快速核对某个符号的链接地址,或在没有DWARF信息时做地址反查。例如:
ffffffff81000000 T _stext
ffffffff81000000 T _text
ffffffff81a00000 T start_kernel
ffffffff81c00000 D init_task
当TRACE32中某个断点地址与预期不符时,可以先用System.map确认该符号的链接地址,再结合内核实际加载基址计算运行地址。System.map与vmlinux必须来自同一次编译,否则地址会整体偏移。
内核模块符号:独立加载与地址空间
内核模块以.ko形式存在,运行时被动态加载到模块地址空间,其符号不在vmlinux中。调试模块需要单独加载.ko的符号。TRACE32支持在模块加载后按模块基址加载符号:
Data.LOAD.ELF my_module.ko /NoCode /NoClear
; 加载模块符号
Data.LOAD.ELF my_module.ko 0xffffffffc0000000
; 按模块实际加载基址重定位
模块基址可以从/proc/modules或cat /sys/module/my_module/sections/.text获取。若模块尚未加载,可以先在do_init_module或模块初始化函数入口下断点,等模块加载后再补加载符号。模块符号加载后,用sYmbol.List my_module确认模块内函数是否可见。
KASLR与地址随机化处理
KASLR(Kernel Address Space Layout Randomization)会在每次启动时随机化内核代码段的加载基址。这带来一个直接问题:vmlinux和System.map中的地址是链接地址,而实际运行地址被随机偏移了。如果TRACE32仍按链接地址下断点,断点就会落在错误位置。
处理KASLR有三种常见方式。第一种是在内核命令行中关闭KASLR,加入nokaslr,这样运行地址等于链接地址,调试最直接,适合开发阶段。第二种是保留KASLR,在启动后从/proc/kallsyms读取_text或start_kernel的实际地址,计算偏移量,再用TRACE32的地址重定位功能加载符号。第三种是在TRACE32中利用内核的kaslr_offset变量或phys_base自动计算偏移。
计算偏移的示例:假设System.map中start_kernel链接地址为0xffffffff81a00000,从目标板/proc/kallsyms读到实际地址为0xffffffff9a400000,则偏移为0xffffffff9a400000 - 0xffffffff81a00000 = 0x18a00000。在TRACE32中可用Data.LOAD.ELF vmlinux 0xffffffff9a400000按实际基址加载,或使用sYmbol.REBASE对已加载符号整体重定位。
调试信息裁剪与恢复
生产内核为了减小体积,常执行strip或编译时使用CONFIG_DEBUG_INFO=n,导致vmlinux丢失DWARF调试信息。此时TRACE32仍能通过符号表看到函数名,但无法显示源码行号、局部变量和结构体成员。恢复调试信息有几种途径:保留一份未裁剪的vmlinux用于调试;使用objcopy --only-keep-debug分离出.debug文件,调试时单独加载;或在内核配置中开启CONFIG_DEBUG_INFO并重新编译。
若只有System.map而无DWARF,仍可做函数级断点和地址级调试,但源码级单步和变量查看会受限。实际项目中建议同时保留vmlinux、System.map和模块.ko的未裁剪版本,并在版本命名上标注内核版本、编译时间和配置,避免符号错配。
版本一致性检查清单
符号错配是内核调试中最隐蔽的问题之一。建议在每次调试前核对以下内容:vmlinux、System.map、.ko是否来自同一次编译;内核版本字符串是否一致(uname -r与编译目录对比);KASLR是否开启以及偏移是否已处理;模块加载基址是否与符号加载基址一致。若使用TRACE32的Data.LOAD.ELF加载符号后断点不命中,优先检查地址重定位是否正确,而不是怀疑断点类型。
nokaslr并保留未裁剪vmlinux,可显著降低符号调试的复杂度。进入生产验证阶段再开启KASLR,并改用偏移重定位方式加载符号。总结来说,vmlinux提供完整符号与调试信息,System.map提供快速地址对照,.ko符号需按模块基址单独加载,KASLR要求地址重定位,调试信息裁剪后需通过保留副本或分离.debug文件恢复。把这五件事在调试前准备好,TRACE32才能把断点、栈回溯和变量查看落到正确的内核代码上。