3. 内核编译与启动流程:交叉编译内核、生成zImage/Image与dtb、QEMU启动内核、内核启动log分析

这一章,我们直接动手。把源码变成能跑的内核,再用QEMU把它点起来。整个过程我走了不下百遍,每次都能发现新坑。今天我把这些经验拆开揉碎,你跟着走一遍,以后自己搞板子心里就有底了。

3.1 交叉编译环境准备

交叉编译,说白了就是在一台电脑上编译出另一台电脑能跑的程序。我们用的是x86的PC,目标却是ARM开发板。这个“翻译官”就是交叉编译工具链。

我的习惯: 我一般用Linaro提供的gcc-arm-none-eabi或gcc-arm-linux-gnueabihf。别自己从源码编译,太耗时。直接下载预编译的二进制包,解压就能用。

设置环境变量,我建议你这样写:

export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
export PATH=$PATH:/opt/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf/bin

注意那个CROSS_COMPILE变量,后面带个短横线。少了它,编译会报找不到命令。我曾经在这个细节上浪费过半小时,后来每次配环境都先echo $CROSS_COMPILE确认一下。

3.2 内核配置与编译

配置内核,我推荐两步走:先用默认配置打底,再用menuconfig微调。

make vexpress_defconfig
make menuconfig

vexpress_defconfig是ARM Versatile Express开发板的默认配置。QEMU模拟的就是这个板子,所以用它最省事。你想想看,如果选错了配置,编译出来的内核可能连串口都打不开,那启动log都看不到。

编译命令很简单:

make -j4 zImage
make -j4 dtbs

-j4表示用4个线程并行编译。我建议别超过CPU核心数的两倍,否则编译反而变慢。

生成的文件:
  • arch/arm/boot/zImage — 压缩后的内核镜像,QEMU直接加载这个
  • arch/arm/boot/dts/vexpress-v2p-ca9.dtb — 设备树二进制文件,描述硬件信息

zImage和Image有什么区别?Image是未压缩的原始内核,zImage是自解压版本。QEMU启动时,zImage会自己解压然后跳转到入口。我一般只用zImage,省空间。

3.3 QEMU启动内核

编译好了,怎么验证?用QEMU模拟。这是我最喜欢的环节,不用烧写、不用接串口线,一条命令就能看到内核跑起来。

qemu-system-arm -M vexpress-a9 \
  -kernel arch/arm/boot/zImage \
  -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
  -nographic \
  -append "console=ttyAMA0,115200 earlycon"

参数说明:

参数 含义
-M vexpress-a9 指定模拟的机器类型,必须与dtb匹配
-kernel 指定zImage路径
-dtb 指定设备树文件
-nographic 不使用图形界面,串口输出到终端
-append 传递内核启动参数
注意: console=ttyAMA0指定了串口设备。不同板子的串口名不一样,vexpress用的是ttyAMA0,有些板子用ttyS0。搞错了,启动log就看不到。

执行后,你会看到内核开始滚动输出。如果一切正常,最后会停在VFS: Cannot open root device。别慌,这是正常的,因为我们没挂文件系统。能看到这一行,说明内核已经成功启动了。

3.4 内核启动流程与log分析

内核启动,我把它分成四个阶段。每个阶段都有对应的log输出,看懂这些log,你就能定位大部分启动问题。

内核启动流程 1. 解压与汇编入口 2. 架构初始化 3. 内核核心初始化 4. 驱动与文件系统 关键log特征: 阶段1: "Uncompressing Linux..." — 解压完成 阶段2: "Booting Linux on physical CPU 0x0" — 进入C代码 阶段3: "Kernel command line: ..." — 解析启动参数 阶段4: "VFS: Cannot open root device" — 挂载根文件系统失败

3.5 earlycon与printk调试

启动参数里我加了earlycon,这个参数很关键。它让内核在早期初始化阶段就能输出log,比正常的console要早得多。

为什么会需要earlycon?因为内核在初始化串口驱动之前,console设备还没注册。如果这时候出问题,你什么都看不到。earlycon绕过驱动,直接操作串口寄存器,把log吐出来。

我的经验: 调试启动阶段的内核崩溃,我第一件事就是加上earlycon。有一次客户说他们的板子启动到一半就黑了,我让他们加上这个参数,log显示是GPIO驱动初始化时访问了非法地址。没有earlycon,这种问题根本没法查。

printk的日志级别也值得一说。内核用printk(KERN_ERR "...")这样的方式输出,级别从高到低:

宏 级别 含义
KERN_EMERG 0 紧急情况,系统不可用
KERN_ALERT 1 需要立即处理
KERN_CRIT 2 严重错误
KERN_ERR 3 错误
KERN_WARNING 4 警告
KERN_NOTICE 5 正常但重要
KERN_INFO 6 信息
KERN_DEBUG 7 调试信息

默认情况下,控制台只显示级别<=4(KERN_WARNING及以上)的log。如果你想看到更多,可以在启动参数里加loglevel=8,或者直接ignore_loglevel。我调试驱动时经常用后者,虽然log会刷屏,但不会漏掉任何线索。

3.6 常见问题与避坑指南

  • 编译报错找不到头文件 — 检查CROSS_COMPILE变量是否设置正确,路径里有没有短横线
  • QEMU启动后黑屏 — 确认-M参数与dtb匹配,vexpress-a9必须用vexpress-v2p-ca9.dtb
  • 看不到启动log — 检查console=参数,vexpress用ttyAMA0,其他板子可能不同
  • 内核卡在"Starting kernel ..." — 很可能是dtb有问题,或者内核配置里没开启对应机器的支持
我曾经踩过的坑: 有一次我编译内核时忘了执行make dtbs,结果用的还是旧版dtb。QEMU启动后内核直接崩溃,log显示"unable to handle kernel paging request"。折腾了半天才发现是dtb没更新。从那以后,我每次编译都写个脚本,把make zImage && make dtbs一起执行。

嗯,这一章的内容就到这里。你跟着走一遍,把zImage和dtb编译出来,再用QEMU跑起来,看看log。下次遇到启动问题,你就知道从哪里下手了。