4. 内核编译与启动:从源码到QEMU跑起来

说实话,很多做驱动的朋友,卡在第一关的不是驱动怎么写,而是——编译出来的内核根本起不来。我自己带团队时,新人最常问的就是:“我改了设备树,为什么系统不认我的外设?”

这一章,我们就亲手走一遍完整流程:编译内核、编译设备树、制作根文件系统,最后用QEMU把整个系统跑起来。你跟着做一遍,后面写驱动时心里就有底了。

4.1 编译内核:选对配置,少走弯路

内核编译本身不复杂,但配置选错了,后面全是坑。我个人习惯用 defconfig 起步,再按需裁剪。

4.1.1 获取源码与配置

# 以 Linux 5.10 LTS 为例
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.210.tar.xz
tar -xf linux-5.10.210.tar.xz
cd linux-5.10.210

# 生成默认配置(ARM64 架构)
make ARCH=arm64 defconfig

# 或者基于当前系统配置微调
make ARCH=arm64 menuconfig
我的经验:第一次编译别贪多。只选你目标平台必需的驱动,否则编译时间会让你怀疑人生。我曾经给一个项目配了全量驱动,编译花了40分钟,后来裁剪到8分钟。

4.1.2 关键配置项

配置项 说明 建议
CONFIG_ARM64 ARM64 架构支持 必选
CONFIG_VIRTIO QEMU 虚拟化设备支持 必选(QEMU 启动用)
CONFIG_EXT4_FS 根文件系统格式 必选
CONFIG_DEVTMPFS 设备节点自动挂载 推荐开启
CONFIG_INITRAMFS_SOURCE 内置 initramfs 路径 按需设置

4.1.3 编译命令

# 编译内核镜像
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image

# 编译设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) dtbs

# 编译内核模块(可选)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) modules

编译完成后,你会得到两个关键产物:arch/arm64/boot/Image 和 arch/arm64/boot/dts/ 下的 .dtb 文件。

注意:交叉编译工具链必须与目标架构匹配。ARM64 用 aarch64-linux-gnu-,ARM32 用 arm-linux-gnueabihf-。混用会导致编译出来的内核无法启动。

4.2 设备树编译:把硬件描述交给内核

设备树(Device Tree)是内核了解硬件的“说明书”。你想想看,如果没有它,内核怎么知道你的板子上有哪些外设、它们挂在哪个地址上?

4.2.1 设备树源文件(.dts)结构

/dts-v1/;

/ {
    model = "QEMU ARM64 Virt Machine";
    compatible = "qemu,virt";

    #address-cells = <2>;
    #size-cells = <2>;

    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x80000000>;
    };

    uart@9000000 {
        compatible = "ns16550a";
        reg = <0x0 0x9000000 0x0 0x1000>;
        interrupts = <0x1>;
        interrupt-parent = <&gic>;
    };

    gic: interrupt-controller@8000000 {
        compatible = "arm,gic-v3";
        reg = <0x0 0x8000000 0x0 0x10000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };
};

这里的关键点:compatible 是内核匹配驱动的“身份证”,reg 描述地址和大小,interrupts 告诉内核中断号。

4.2.2 编译与反编译

# 编译 .dts 为 .dtb
dtc -I dts -O dtb -o qemu-virt.dtb qemu-virt.dts

# 反编译 .dtb 查看内容
dtc -I dtb -O dts -o qemu-virt.dts qemu-virt.dtb
避坑指南:我曾经在项目里手写了一个 .dts,结果 UART 地址写错了,内核启动时串口完全没输出。排查了整整半天才发现是 reg 少写了一个 0。所以,写完设备树一定要反编译验证,看看地址对不对。

4.3 制作根文件系统:让内核有“家”可归

内核启动后,需要挂载根文件系统才能执行用户态程序。最简单的做法是用 BusyBox 制作一个最小根文件系统。

4.3.1 使用 BusyBox 构建

# 下载并配置 BusyBox
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install

4.3.2 构建目录结构

mkdir -p rootfs/{bin,sbin,etc,dev,proc,sys,usr/{bin,sbin},lib,lib64,mnt,root,tmp,var}
cp -a _install/* rootfs/

# 创建必要的设备节点
sudo mknod rootfs/dev/console c 5 1
sudo mknod rootfs/dev/null c 1 3

# 创建 init 脚本
cat << 'EOF' > rootfs/init
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "Welcome to Minimal Rootfs!"
exec /bin/sh
EOF
chmod +x rootfs/init

4.3.3 打包为 initramfs

cd rootfs
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz

这个 initramfs.cpio.gz 就是内核启动时直接加载到内存的根文件系统。不需要磁盘,非常适合 QEMU 快速验证。

核心思路:initramfs 是“内存中的根文件系统”。你写的驱动模块可以放在里面,内核启动时自动加载,非常适合驱动开发阶段的快速迭代。

4.4 QEMU 模拟启动:验证你的劳动成果

所有材料都准备好了,现在用 QEMU 把它们串起来。

4.4.1 启动命令

qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -m 2G \
    -kernel linux-5.10.210/arch/arm64/boot/Image \
    -dtb qemu-virt.dtb \
    -initrd initramfs.cpio.gz \
    -nographic \
    -append "console=ttyAMA0 root=/dev/ram rdinit=/init"

参数说明:

  • -machine virt:QEMU 的 ARM64 虚拟平台
  • -kernel:指定内核镜像
  • -dtb:指定设备树
  • -initrd:指定 initramfs
  • -nographic:不使用图形界面,用终端交互
  • -append:传递给内核的启动参数

4.4.2 启动日志解读

启动后你会看到内核打印大量信息。重点关注这几行:

[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd051]
[    0.000000] Machine model: QEMU ARM64 Virt Machine
[    0.123456] Serial: 8250/16550 driver, 4 ports, IRQ sharing enabled
[    0.234567] Freeing initrd memory: 4096K
[    0.345678] VFS: Mounted root (ramfs filesystem) on device 0:15
[    0.456789] Freeing unused kernel memory: 1024K
[    0.567890] Run /init as init process
Welcome to Minimal Rootfs!
/ #

看到 / # 提示符,说明系统启动成功了!你现在就在一个完整的 Linux 环境里,可以加载驱动模块、调试设备。

常见问题:如果卡在 Kernel panic - not syncing: VFS: Unable to mount root fs,通常是 initramfs 路径不对,或者内核没有开启 CONFIG_BLK_DEV_INITRD。回去检查配置。

4.5 本章知识体系

下面这张图帮你理清整个流程的依赖关系:

内核编译与启动流程 Linux 内核源码 设备树源文件 (.dts) BusyBox 源码 make Image dtc 编译 make install Image (内核镜像) .dtb (设备树) initramfs.cpio.gz QEMU 模拟启动 ✅ 系统启动成功,进入 Shell

4.6 本章小结

我们走完了从源码到启动的完整链路:

  • 编译内核:配置、裁剪、交叉编译,得到 Image
  • 编译设备树:写 .dts、用 dtc 生成 .dtb,描述硬件布局
  • 制作根文件系统:BusyBox 构建、目录结构、打包 initramfs
  • QEMU 启动:一条命令把三样东西组合起来,验证系统是否正常

这套流程我用了很多年。每次拿到一个新板子,第一件事就是跑通这个最小系统。只有基础环境稳了,后面写驱动才敢放手去干。

给你的建议:把本章的步骤写成脚本,一键编译+启动。这样每次修改驱动后,跑一下脚本就能验证,效率翻倍。