4. QEMU目标板搭建
根文件系统,说白了就是 Linux 启动后挂载的第一个文件系统。它里面装着 init 进程、shell、各种库和工具。没有它,内核就是个空壳子。在 QEMU 上调试汽车 Linux,你不可能每次都烧写整块板子——太慢了。所以,学会用 Buildroot 和 Yocto 构建轻量级根文件系统,再通过 NFS 挂载调试,是每个嵌入式工程师的必修课。
我个人习惯在项目早期用 Buildroot 快速验证,到了产品化阶段再切到 Yocto。为什么?Buildroot 简单粗暴,Yocto 灵活但复杂。下面我带你走一遍这两个工具的核心流程。
4.1 使用 Buildroot 构建轻量级根文件系统
Buildroot 的理念是「一键编译」。你配置好目标架构、工具链、需要的软件包,它就能生成完整的根文件系统镜像。我在项目中遇到过好几次,开发板还没到货,先用 Buildroot 在 QEMU 上把驱动和业务逻辑调通了,板子一到直接烧写,省了至少两周时间。
4.1.1 下载与配置
# 下载 Buildroot(我用的是 2023.11 LTS 版本)
wget https://buildroot.org/downloads/buildroot-2023.11.tar.gz
tar xf buildroot-2023.11.tar.gz
cd buildroot-2023.11
# 配置目标架构(以 ARM 为例)
make qemu_arm_versatile_defconfig
make menuconfig
在 menuconfig 里,你需要关注几个关键项:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Target Architecture | ARM (little endian) | 匹配 QEMU 模拟的 CPU |
| Target Architecture Variant | arm926t | Versatile PB 板用的内核 |
| Toolchain type | Buildroot internal | 自动编译交叉工具链 |
| System configuration | Enable root login with password | 设个密码,方便调试 |
| Target packages | 选中 dropbear、strace、gdb | SSH、调试工具必备 |
BR2_JLEVEL 设成你 CPU 核心数+1,能大幅缩短编译时间。我一般设成 8。
4.1.2 编译与输出
make -j$(nproc)
编译完成后,在 output/images/ 目录下你会看到:
rootfs.ext2—— 根文件系统镜像,QEMU 可以直接用zImage—— 内核镜像versatile-pb.dtb—— 设备树
启动命令很简单:
qemu-system-arm -M versatilepb \
-kernel output/images/zImage \
-dtb output/images/versatile-pb.dtb \
-drive file=output/images/rootfs.ext2,if=scsi,format=raw \
-append "root=/dev/sda console=ttyAMA0" \
-nographic
看到登录提示符的那一刻,你就拥有了一个完整的、可交互的汽车 Linux 调试环境。
make menuconfig 的 Filesystem images 里调大 ext2/3/4 size。我曾经因为没调这个,在 QEMU 上跑 AGL 的 demo 时直接 OOM,排查了半天才发现是根文件系统空间不够。
4.2 Yocto Project 基础与汽车层
Yocto 比 Buildroot 复杂得多,但它的可定制性和生态是 Buildroot 无法比的。尤其是汽车行业,AGL(Automotive Grade Linux)和 Renesas 的 BSP 都是基于 Yocto 的。你想想看,一个项目里可能有十几个不同的 ECU,每个的软件栈都不一样,Yocto 的层(layer)机制能让你优雅地管理这些差异。
4.2.1 Yocto 核心概念
Yocto 的构建系统叫 BitBake,它读取 配方(recipe) 和 层(layer) 来生成镜像。我刚开始学 Yocto 时,被它的目录结构搞得晕头转向。后来我总结了一个口诀:
- meta-* —— 层,每个层包含一组配方和配置
- .bb —— 配方文件,描述如何编译一个软件包
- .bbappend —— 配方追加文件,用来修改已有配方
- .conf —— 配置文件,定义全局变量
一个典型的 Yocto 项目结构长这样:
my-yocto-project/
├── poky/ # 核心层(包含 BitBake、OpenEmbedded 核心)
├── meta-openembedded/ # 额外软件包层
├── meta-renesas/ # Renesas 板级支持层
├── meta-agl/ # AGL 汽车层
├── meta-my-company/ # 你自己的应用层
└── build/ # 构建目录
└── conf/
├── local.conf # 本地配置(目标架构、镜像类型等)
└── bblayers.conf # 层列表
4.2.2 集成 AGL 与 Renesas 层
假设你要为 Renesas R-Car M3 开发一个 AGL 仪表盘。你需要拉取以下层:
git clone git://git.yoctoproject.org/poky.git -b dunfell
git clone git://git.openembedded.org/meta-openembedded.git -b dunfell
git clone https://github.com/renesas-rcar/meta-renesas.git -b dunfell
git clone https://gerrit.automotivelinux.org/gerrit/AGL/meta-agl.git -b dunfell
在 bblayers.conf 里添加这些层:
BBLAYERS ?= " \
/path/to/poky/meta \
/path/to/poky/meta-poky \
/path/to/meta-openembedded/meta-oe \
/path/to/meta-openembedded/meta-python \
/path/to/meta-renesas \
/path/to/meta-agl/meta-agl-core \
/path/to/meta-agl/meta-agl-demo \
"
然后在 local.conf 里指定目标:
MACHINE = "rcar-m3-salvator-x"
DISTRO = "agl-demo"
IMAGE_INSTALL_append = " packagegroup-agl-demo-platform"
meta-my-company 放在最后,确保我们的定制能覆盖所有上游配置。
4.2.3 构建与输出
bitbake agl-demo-platform
第一次构建会下载所有源码并编译,耗时取决于网络和机器性能。我见过最夸张的一次,在 8 核机器上跑了 6 个小时。所以,我建议你第一次构建时用 bitbake -k 让它在出错时继续,至少能拿到部分输出。
构建完成后,镜像在 build/tmp/deploy/images/rcar-m3-salvator-x/ 下:
agl-demo-platform-rcar-m3-salvator-x.wic—— 完整的 SD 卡镜像zImage—— 内核Image—— 如果用了 64 位 ARM*.dtb—— 设备树
4.3 通过 NFS 挂载根文件系统进行调试
为什么要在 QEMU 上用 NFS 挂载根文件系统?因为快。你修改了应用代码,不需要重新编译整个镜像、重启 QEMU。只需要在宿主机上更新文件,QEMU 里的系统就能立刻看到变化。我在调试 AGL 的 Window Manager 时,一天要改几十次代码,NFS 挂载让我每次修改后只需重启窗口管理器进程,而不是整个虚拟机。
4.3.1 宿主机配置 NFS 服务
# 安装 NFS 服务器(以 Ubuntu 为例)
sudo apt install nfs-kernel-server
# 创建导出目录,把 Buildroot 或 Yocto 的根文件系统解压到这里
sudo mkdir -p /srv/nfs/rootfs
sudo tar xf output/images/rootfs.ext2 -C /srv/nfs/rootfs
# 配置 exports 文件
echo "/srv/nfs/rootfs *(rw,no_root_squash,no_subtree_check)" | sudo tee -a /etc/exports
# 重启 NFS 服务
sudo exportfs -a
sudo systemctl restart nfs-kernel-server
no_root_squash 允许 QEMU 里的 root 用户以 root 权限访问 NFS 共享。这在调试时很方便,但生产环境千万别这么干。我曾经在客户现场演示时忘了关这个选项,结果被安全审计抓了个正着。
4.3.2 QEMU 启动参数
QEMU 启动时,通过 -append 参数告诉内核从 NFS 挂载根文件系统:
qemu-system-arm -M versatilepb \
-kernel output/images/zImage \
-dtb output/images/versatile-pb.dtb \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0 \
-append "root=/dev/nfs nfsroot=10.0.2.2:/srv/nfs/rootfs rw ip=dhcp console=ttyAMA0" \
-nographic
这里的关键参数:
| 参数 | 含义 |
|---|---|
root=/dev/nfs |
告诉内核使用 NFS 作为根文件系统 |
nfsroot=10.0.2.2:/srv/nfs/rootfs |
NFS 服务器地址和共享路径。10.0.2.2 是 QEMU 用户模式网络下宿主机的默认网关 |
ip=dhcp |
让 QEMU 虚拟机通过 DHCP 获取 IP 地址 |
4.3.3 调试工作流
启动后,你可以在宿主机上直接修改 /srv/nfs/rootfs 下的文件。比如,更新一个动态库:
# 宿主机上编译
aarch64-linux-gnu-gcc -shared -o libmyapp.so myapp.c
# 复制到 NFS 共享目录
cp libmyapp.so /srv/nfs/rootfs/usr/lib/
# 在 QEMU 虚拟机里重启应用
ssh root@localhost -p 2222 "killall myapp; /usr/bin/myapp &"
整个过程不需要重启 QEMU,不需要重新烧写镜像。这就是 NFS 挂载的魅力。
strace 和 gdb 使用 NFS 挂载,调试效率翻倍。我通常会在 QEMU 里跑 strace -p <pid> 跟踪系统调用,同时在宿主机上用 gdb-multiarch 远程调试。两个窗口一对比,问题出在哪一目了然。
4.4 本章知识结构图
下面这张图总结了本章的核心逻辑:从构建工具选型到最终调试环境的完整链路。
从这张图你可以看到,无论你用 Buildroot 还是 Yocto,最终都要落到「怎么把根文件系统喂给 QEMU」。我个人强烈建议你在学习阶段优先掌握 NFS 挂载——它带来的迭代速度提升是立竿见影的。等你把应用调稳了,再切回块设备镜像做最终验证。