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 本章知识体系
下面这张图帮你理清整个流程的依赖关系:
4.6 本章小结
我们走完了从源码到启动的完整链路:
- 编译内核:配置、裁剪、交叉编译,得到
Image - 编译设备树:写
.dts、用dtc生成.dtb,描述硬件布局 - 制作根文件系统:BusyBox 构建、目录结构、打包 initramfs
- QEMU 启动:一条命令把三样东西组合起来,验证系统是否正常
这套流程我用了很多年。每次拿到一个新板子,第一件事就是跑通这个最小系统。只有基础环境稳了,后面写驱动才敢放手去干。
给你的建议:把本章的步骤写成脚本,一键编译+启动。这样每次修改驱动后,跑一下脚本就能验证,效率翻倍。