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是真正被搬运到目标内存的二进制。两者必须来自同一次编译,否则符号地址与运行地址会错位,断点会落在错误位置。
启动参数的传递方式
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,以便在早期启动阶段获得更多输出。
早期启动暂停的实现
内核入口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路径,就能在几十秒内复现“加载—传参—暂停—衔接”的完整过程。这样,后续分析内核初始化、驱动探测或崩溃现场时,才有一个稳定、可重复的起点。