4. 流水线结构解析

内核启动阶段,说白了就是系统从“裸机”到“有操作系统”的惊险一跃。这个阶段出问题,你连个登录提示都看不到。我在 Jacinto 平台上调试过不少启动失败的问题,今天就把这些经验掰开揉碎讲给你听。

4.1 内核解压与启动流程全景

TI Jacinto 平台(如 DRA8x/TDA4x)上,内核启动大致分这么几步:

  1. Boot ROM 加载 SPL(Secondary Program Loader)
  2. SPL 初始化 DDR,加载 U-Boot
  3. U-Boot 加载内核镜像(zImage/Image)到 DDR
  4. 内核自解压(如果是压缩镜像)
  5. 内核入口 start_kernel() 开始执行
  6. 早期初始化:中断、定时器、控制台
  7. 驱动初始化:do_initcalls()
  8. 挂载根文件系统,启动 init 进程

这里面最容易出问题的,就是第 4 步到第 6 步。内核还没打印任何信息就挂了,这种问题最让人头疼。

核心要点: 内核解压阶段如果出错,你连 earlycon 都看不到。这时候只能靠硬件调试器(JTAG)或者 U-Boot 的 bootm 命令返回值来定位。

TI Jacinto 内核启动流程 Boot ROM SPL U-Boot 内核解压 start_kernel init 进程 常见问题点: • 解压地址冲突:内核解压地址与 DTB 地址重叠 • 早期串口未初始化:earlycon 参数配置错误 • 缓存一致性:MMU 开启后外设地址空间未正确映射 • 时钟/电源:关键外设时钟未使能导致系统挂死

4.2 早期串口打印(earlycon)—— 你的第一双眼睛

内核还没完全初始化时,console 驱动还没加载。这时候想看打印信息,就得靠 earlycon。

4.2.1 earlycon 工作原理

earlycon 在 setup_arch() 阶段就初始化了串口。它不依赖设备模型,直接操作寄存器。说白了,就是绕过所有驱动框架,直接往串口 FIFO 里写数据。

在 Jacinto 平台上,典型的 earlycon 配置是这样的:

# 在 U-Boot bootargs 中添加
earlycon=uart8250,mmio32,0x02800000,115200n8
# 或者使用设备树中的 stdout-path
earlycon

我的经验: 我习惯在调试阶段同时保留 earlycon 和 console。earlycon 负责早期打印,console 接管后续输出。这样即使 console 驱动初始化失败,你还能看到最后一条日志。

4.2.2 常见问题与排查

现象 可能原因 排查方法
earlycon 无输出 串口基地址错误 核对 TRM 中 UART 基地址
输出乱码 波特率不匹配 检查 U-Boot 和内核波特率设置
输出到一半卡住 时钟门控导致串口掉电 检查 PRCM 模块时钟配置
earlycon 有输出,console 无输出 设备树中 serial 节点 status 为 disabled 检查设备树 aliases 和 chosen 节点

注意: earlycon 使用的是固定波特率,如果 U-Boot 修改了串口时钟频率,earlycon 的波特率计算会出错。我曾经遇到过 U-Boot 把 UART 时钟从 48MHz 改到 38.4MHz,结果 earlycon 输出全是乱码,折腾了半天才发现是时钟问题。

4.3 内核 Oops 分析与栈回溯

内核崩溃了怎么办?Oops 信息就是你的“黑匣子”。

4.3.1 读懂 Oops 信息

一个典型的 Oops 包含这些关键字段:

Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 5 [#1] PREEMPT SMP ARM
Modules linked in: my_driver
CPU: 0 PID: 42 Comm: kworker/u8:2 Not tainted 5.10.100 #1
Hardware name: TI Jacinto J7
PC is at my_function+0x14/0x30 [my_driver]
LR is at worker_thread+0x1a0/0x2a0
pc : [<bf000014>]    lr : [<c0101234>]    psr: 60000013
sp : c7827d80  ip : c7827d90  fp : c7827d9c
r10: 00000000  r9 : c7826000  r8 : c7827e00
r7 : 00000000  r6 : c7827e00  r5 : bf000000  r4 : c7827e00
r3 : 00000000  r2 : 00000001  r1 : 00000000  r0 : 00000000
Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
Control: 10c5387d  Table: 4a8a806a  DAC: 00000055
Process kworker/u8:2 (pid: 42, stack limit = 0xc7826280)
Stack: (0xc7827d80 to 0xc7828000)
...
[<bf000014>] (my_function) from [<c0101234>] (worker_thread+0x1a0/0x2a0)
[<c0101234>] (worker_thread) from [<c0100e98>] (kthread+0x118/0x150)
[<c0100e98>] (kthread) from [<c0100114>] (ret_from_fork+0x14/0x20)
Code: e1a04000 e3a03000 e5843000 e3a03001 (e5843004)

怎么看?我教你三步法:

  1. 看错误类型:NULL pointer dereference、page fault、prefetch abort
  2. 看 PC 指针:PC is at my_function+0x14/0x30,说明在 my_function 偏移 0x14 处出错
  3. 看调用栈:从下往上读,找到你写的代码

4.3.2 栈回溯分析技巧

有时候 Oops 信息不完整,或者你只有一串地址。这时候可以用 addr2line 或 gdb 来解析:

# 使用 addr2line 解析地址
arm-linux-gnueabihf-addr2line -e vmlinux -f c0101234

# 或者用 gdb 加载 vmlinux 后查看
(gdb) list *0xc0101234

关键配置: 编译内核时一定要开启 CONFIG_DEBUG_INFO 和 CONFIG_KALLSYMS。否则 Oops 里只有地址,没有函数名,分析起来非常痛苦。我见过有人没开这些选项,拿着地址列表一个个手动查 System.map,效率极低。

4.4 Kdump 与 Kexec 机制

系统崩溃后自动重启,然后分析上次崩溃的原因——这就是 Kdump 的价值。

4.4.1 Kexec 快速重启

Kexec 跳过 BIOS/U-Boot,直接加载新内核。在 Jacinto 平台上,重启时间可以从 30 秒缩短到 3 秒以内。

# 安装 kexec-tools
# 手动触发 kexec
kexec -l /boot/zImage --initrd=/boot/initrd.img --append="root=/dev/mmcblk0p2"
kexec -e

4.4.2 Kdump 配置与使用

Kdump 需要预留一块内存给捕获内核。在 Jacinto 平台上,我建议预留 128MB~256MB:

# 内核启动参数中添加
crashkernel=128M@0x48000000

# 或者使用自动分配
crashkernel=128M

配置完成后,触发一次 crash 测试:

# 触发 crash(仅测试用)
echo c > /proc/sysrq-trigger

# 系统重启后,在 /var/crash 目录下找到 vmcore
# 使用 crash 工具分析
crash vmlinux vmcore
参数 说明 Jacinto 平台建议值
crashkernel 预留内存大小和地址 128M@0x48000000
irqpoll 捕获内核中轮询中断 必须添加
nr_cpus=1 捕获内核只使用单核 建议添加
reset_devices 重置设备状态 建议添加

注意: 在 Jacinto 平台上,如果使用 GIC(通用中断控制器),捕获内核需要正确处理中断路由。我遇到过捕获内核启动后立即死机的情况,最后发现是 GIC 中断没有正确屏蔽,导致捕获内核被残留的中断信号干扰。解决方案是在 kexec 前调用 gic_dist_disable()。

4.5 实战:一次启动失败的完整排查

讲个真实案例。有一次客户反馈,Jacinto J7 开发板在低温环境下(-20°C)启动概率性失败。现象是:U-Boot 正常,内核解压完成,但 earlycon 输出到 "Starting kernel ..." 后就没了。

排查过程:

  1. 加长 earlycon 输出:在 start_kernel() 开头加 printk,确认代码执行到哪里
  2. 发现卡在 mm_init():内存初始化时访问了未稳定的 DDR 区域
  3. 检查 DDR 初始化时序:低温下 DDR PHY 训练参数需要调整
  4. 修复:在 SPL 中增加 DDR 重训练逻辑,低温时使用更保守的时序参数

我的建议: 遇到启动阶段的问题,优先用 earlycon 缩小范围。如果 earlycon 都没输出,检查 U-Boot 的 bootm 命令返回值,或者用 JTAG 直接读 PC 指针。不要盲目改代码,先确认问题在哪一层。

内核启动调试,说白了就是“分层排查、逐级确认”。从 Boot ROM 到 init 进程,每一层都有对应的调试手段。掌握了 earlycon、Oops 分析和 Kdump,你就有了三把利器,大部分启动问题都能搞定。