第1章:内核早期启动问题排查

大家好,我是老李。做嵌入式Linux开发这么多年,我踩过最多的坑,就是内核刚启动那会儿。说白了,这时候系统就像刚出生的婴儿,啥都还没准备好——内存没初始化、串口没通、打印信息都出不来。你想想看,连个报错都看不到,怎么查?

这一章,我就把内核早期启动的几个典型问题掰开揉碎了讲。包括解压阶段出错、MMU初始化翻车、早期串口哑火,还有最让人头疼的内核恐慌(Kernel Panic)。每个问题我都会结合项目中的真实案例,给你一套可操作的排查思路。

核心观点:早期启动问题,90%靠的是“盲调”——没有打印,没有调试器,全靠对硬件和启动流程的深刻理解。我建议你先把内核启动的汇编阶段和C语言入口之间的过渡搞明白,后面就顺了。

1.1 解压阶段错误分析

内核镜像通常是被压缩的。Bootloader加载完zImage或Image.gz后,会跳转到解压代码。解压失败,系统直接卡死。我在项目中遇到过好几次,板子焊回来一上电,串口啥都没有,最后发现是解压地址冲突了。

常见解压错误类型

错误现象 可能原因 排查方向
解压后跳转地址不对 链接脚本中解压地址与运行地址不一致 检查arch/arm/boot/compressed/vmlinux.lds
CRC校验失败 镜像被破坏或DDR不稳定 重新烧录,检查DDR时序配置
解压过程中断 Cache未关闭导致数据不一致 确认解压前已关闭MMU和Cache
解压后内核无法执行 解压地址覆盖了Bootloader或设备树 调整解压地址,避开冲突区域

嗯,这里要注意。解压阶段的代码是位置无关的(PIC),但解压后的内核是有固定链接地址的。我曾经在移植某款RISC-V芯片时,发现解压后内核总是跑飞。查了两天,最后发现是解压缓冲区设在了DDR的保留区域,被Bootloader占用了。改了个地址就好了。

个人技巧:我习惯在解压代码入口处加一段GPIO点灯代码。如果灯亮了但没后续,说明解压前是好的,问题出在解压过程中。这比干瞪眼强多了。

1.2 MMU初始化问题

MMU初始化是内核早期启动中最容易翻车的环节之一。说白了,就是页表没配好,导致虚拟地址和物理地址对不上。系统一访问内存就崩。

MMU初始化通常在head.S或head.S的汇编阶段完成。核心步骤包括:

  1. 创建一级页表(页目录)
  2. 设置页表项,映射内核代码段、数据段、堆栈等
  3. 使能MMU,同时保持TLB和Cache的干净
  4. 跳转到虚拟地址空间中的C语言入口

我遇到过最典型的问题:页表项中的内存属性设置错误。比如把设备内存区域设成了Cacheable,结果访问外设寄存器时读到的全是缓存里的旧数据。调试这种问题,我建议你用JTAG直接读物理地址,对比虚拟地址的返回值,一眼就能看出问题。

警告:MMU使能前后,PC指针必须从物理地址平滑过渡到虚拟地址。很多新手在这里翻车——使能MMU后,下一条指令还在物理地址空间,但MMU已经开始翻译了,直接导致指令预取异常。解决办法是在使能MMU后紧跟一条跳转指令,跳转到虚拟地址的标签处。

MMU初始化检查清单

  • 页表基地址是否对齐到16KB?
  • 内核代码段是否映射为可执行+可读?
  • 设备内存是否映射为Strongly-Ordered或Device类型?
  • 使能MMU前是否清空了TLB和Branch Predictor?
  • 跳转目标地址是否在虚拟地址空间中?

1.3 早期串口无法打印

早期串口打印,是内核启动阶段我们唯一的“眼睛”。如果串口没输出,基本等于盲调。我见过太多人一上来就怀疑串口硬件坏了,其实多半是软件配置问题。

早期串口无法打印的常见原因:

  • 时钟未使能:UART模块的时钟在Bootloader里可能没开,或者内核早期代码没重新使能。我建议你在串口初始化函数开头,先读一下时钟控制寄存器的值。
  • 引脚复用错误:芯片的引脚功能需要配置。比如某款国产芯片的UART0和GPIO共用引脚,默认是GPIO模式,必须手动切到UART模式。
  • 波特率不匹配:这个最坑。Bootloader和内核用的可能是不同的时钟源,导致波特率计算偏差。我习惯在串口驱动里加一个“波特率回读”功能,打印出实际配置的波特率值。
  • 打印缓冲未刷新:早期串口驱动通常用轮询模式,但如果误开了FIFO或中断,打印字符可能被缓存。解决办法是强制使用轮询模式,并清空FIFO。

排查步骤:先确认Bootloader阶段串口是否正常。如果Bootloader能打印,但内核不行,那问题就在内核的串口初始化代码。如果Bootloader也不行,先查硬件连接和时钟。

1.4 内核恐慌(Kernel Panic)定位

Kernel Panic,说白了就是内核遇到了无法恢复的错误,直接躺平。早期启动阶段的Panic尤其难搞,因为这时候连console都没准备好,错误信息可能根本看不到。

我总结了一套定位方法:

  1. 抓取完整日志:如果串口还能输出,用dmesg或串口工具把日志完整抓下来。注意,Panic前的最后几行往往就是关键线索。
  2. 分析调用栈:Panic信息里通常会打印出函数调用栈。根据PC值反查System.map或vmlinux的符号表,定位到具体函数。
  3. 检查关键初始化顺序:很多Panic是因为某个子系统初始化时依赖的资源还没准备好。比如在中断控制器初始化之前就注册了中断处理函数。
  4. 使用早期调试手段:在可疑函数入口加printk,或者用GPIO点灯标记执行路径。我习惯在start_kernel里每隔几个关键函数就加一个点灯操作,这样即使Panic了,也能知道最后执行到哪一步。

我的经验:有一次移植某款MIPS芯片,内核总是在setup_arch里Panic。查了两天,最后发现是设备树中内存节点的大小字段写错了,导致内核认为内存只有1MB。嗯,这种低级错误,往往最隐蔽。

知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以把它当作排查时的路线图。

内核早期启动问题排查知识体系 内核启动入口 解压阶段 地址冲突 CRC校验失败 Cache不一致 MMU初始化 页表错误 内存属性错误 早期串口 时钟/引脚问题 波特率不匹配 Kernel Panic

这张图展示了内核早期启动的四个关键阶段,以及每个阶段可能遇到的问题。排查时,你可以按照这个顺序逐级检查。我个人习惯是从串口入手——先确保能看到输出,再回头查解压和MMU的问题。

总结一下:早期启动问题排查,核心是“分阶段隔离”。把启动过程切成解压、MMU、串口、内核初始化四个阶段,每个阶段用不同的手段验证。解压阶段靠GPIO点灯,MMU阶段靠JTAG读物理地址,串口阶段靠Bootloader对比,Kernel Panic靠调用栈分析。这套方法,我在多个国产芯片项目上都验证过,很管用。

专注资料整理