4. TRACE32加载与运行Linux内核

当Bootloader完成DDR初始化、串口与存储驱动准备之后,调试工作的重心就从“让板子跑起来”转移到“让内核在受控条件下跑起来”。这一节要解决的核心问题是:如何用TRACE32把Linux内核镜像放到目标内存的正确位置,如何把Bootloader已经准备好的启动参数交给内核,如何在早期启动阶段就停下来观察,以及如何让Bootloader与内核入口之间的调试会话不断链。

内核镜像的构成与加载位置

嵌入式Linux常见的可引导内核镜像有两类:一类是压缩自解压镜像,如ARM架构下的zImage、Image.gz,另一类是未压缩的Image或vmlinux导出的二进制。压缩镜像的入口是一段自解压头,运行后会把自己解压到内存高端再跳转;未压缩镜像则直接以内核虚拟地址布局运行。调试时选择哪一种,直接决定了断点能否在“内核第一条指令”处命中。

以典型的ARM64平台为例,Bootloader通常把内核镜像加载到DDR中一个物理地址,例如0x40080000,并通过设备树/chosen节点或寄存器传递启动信息。在TRACE32中,加载动作可以用Data.LOAD系列命令完成:

Data.LOAD.Elf vmlinux /NoClear /NoCopy
Data.LOAD.Binary Image 0x40080000
Register.Set PC 0x40080000
Register.Set X0 0x40000000   ; DTB物理地址

这里vmlinux是带符号的ELF文件,用于提供符号与调试信息;Image是真正被搬运到目标内存的二进制。两者必须来自同一次编译,否则符号地址与运行地址会错位,断点会落在错误位置。

TRACE32 加载内核与启动参数传递链路 主机侧文件 vmlinux(符号+调试信息) Image / zImage(可执行镜像) board.dtb(设备树) TRACE32 调试器 Data.LOAD.Elf / .Binary Register.Set PC / X0 Breakpoint 设置与命中 目标 DDR 内存 0x40080000 内核镜像 0x40000000 设备树 DTB 0x48000000 保留区 Bootloader 初始化 DDR / 串口 准备 DTB 与启动参数 内核入口 head.S / stext X0=DTB, X1..X3=保留 早期启动 start_kernel() setup_arch / 解析 DTB 早期启动暂停点 stext / start_kernel earlyprintk / earlycon 调试衔接 Bootloader 断点 → 内核入口断点 观察与判断 PC 是否命中入口 X0 是否指向 DTB

启动参数的传递方式

Linux内核在ARM64上的启动约定是:X0寄存器保存设备树二进制(DTB)的物理地址,X1至X3保留为0。内核通过解析DTB中的/chosen节点获取bootargs,因此“启动参数传递”在调试语境下包含两层含义:一是寄存器层面的入口约定,二是DTB内容层面的参数注入。

如果Bootloader已经把DTB放在0x40000000,那么在TRACE32中跳转到内核入口前,必须确认X0的值。可以用Register.View或Register.Set直接检查与修改。若DTB尚未加载,可以先用Data.LOAD.Binary board.dtb 0x40000000把它放到约定地址,再设置X0。对于需要临时覆盖bootargs的场景,可以在DTB中定位/chosen/bootargs属性,用Data.Set修改字符串内容,例如把console=ttyAMA0,115200改为console=ttyAMA0,115200 earlycon loglevel=8,以便在早期启动阶段获得更多输出。

调试时建议同时保留“Bootloader传递的DTB”和“TRACE32直接加载的DTB”两条路径。前者用于验证真实启动流程,后者用于快速替换参数、隔离问题。

早期启动暂停的实现

内核入口stext或_text是早期启动暂停的首选位置。在TRACE32中,可以在加载符号后直接对stext设置软件断点或硬件断点:

Break.Set stext /Program
Break.Set start_kernel /Program
Go

如果目标平台支持硬件断点,优先使用/Hardware,因为早期启动阶段MMU尚未开启,软件断点需要改写内存指令,可能受到只读映射或缓存一致性的影响。命中stext后,先检查PC、X0和当前异常级别CurrentEL,确认内核确实从预期的物理地址开始执行。随后可以单步执行若干条指令,观察是否进入el2_setup、set_cpu_boot_mode_flag等早期路径。

若希望更早看到内核打印,可以在bootargs中加入earlycon,并在start_kernel处设置第二个断点。这样调试会话就形成两个观察窗口:入口处看“参数是否正确”,start_kernel处看“早期初始化是否走通”。

从Bootloader到内核入口的调试衔接

实际调试中最容易断链的地方,是Bootloader跳转到内核的瞬间。常见做法是在Bootloader的最后一条跳转指令处设置断点,命中后不要立即Go,而是先完成三件事:确认内核镜像已经就位、确认X0指向有效DTB、确认内核入口地址与符号表一致。完成后再单步或继续运行到stext。

如果使用TRACE32的SYStem.Mode进行复位调试,需要注意复位后Bootloader会重新运行,之前设置的断点可能被覆盖。此时应使用Break.Set /Onchip或把断点设置在Bootloader不会改写的区域。另一种稳妥方式是利用TRACE32的PRACTICE脚本,把“加载镜像—设置寄存器—设置断点—运行”串成一条可重复执行的命令序列:

Data.LOAD.Elf vmlinux /NoClear
Data.LOAD.Binary Image 0x40080000
Data.LOAD.Binary board.dtb 0x40000000
Register.Set X0 0x40000000
Break.Set stext /Hardware
Break.Set start_kernel /Program
Go

判断衔接是否成功,可以看三个信号:stext断点是否命中、命中时PC是否等于符号地址、X0是否等于DTB物理地址。三者一致,说明从Bootloader到内核入口的调试链路已经建立;若stext未命中而系统直接跑飞,则应回退检查镜像加载地址、入口偏移和CPU异常级别是否符合内核启动约定。

压缩镜像zImage的入口并不是最终内核入口,自解压完成后会跳转到解压后的地址。若在zImage入口设断点后直接单步,可能进入解压代码而非内核主体。调试早期启动时,优先使用未压缩Image或直接对解压后的stext下断点。

关键检查点小结

检查项预期值/现象异常时的排查方向
内核镜像加载地址与链接地址一致,如0x40080000核对Image与vmlinux是否同次编译
X0寄存器指向有效DTB物理地址检查DTB是否加载、地址是否被覆盖
stext断点命中且PC等于符号地址确认使用未压缩镜像或解压后地址
start_kernel断点命中且串口有earlycon输出检查bootargs中earlycon与loglevel
异常级别与内核配置的启动EL一致检查Bootloader是否按约定设置EL

把上述动作固化成脚本后,每次调试只需替换镜像路径和DTB路径,就能在几十秒内复现“加载—传参—暂停—衔接”的完整过程。这样,后续分析内核初始化、驱动探测或崩溃现场时,才有一个稳定、可重复的起点。