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 调试环境。

注意: Buildroot 默认的 rootfs.ext2 只有 64MB。如果你的应用需要更多空间,在 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"
关键点: Yocto 的层顺序很重要。后面的层会覆盖前面的层。我一般把 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 Project 汽车层集成 rootfs.ext2 / .cpio .wic / .tar.bz2 镜像 meta-agl + meta-renesas 本地块设备挂载 NFS 网络挂载 initramfs 内存挂载 串口 / SSH 登录 strace + gdb 远程调试 日志 / perf 分析 QEMU 汽车 Linux 调试环境就绪 NFS 挂载:修改即生效,无需重启

从这张图你可以看到,无论你用 Buildroot 还是 Yocto,最终都要落到「怎么把根文件系统喂给 QEMU」。我个人强烈建议你在学习阶段优先掌握 NFS 挂载——它带来的迭代速度提升是立竿见影的。等你把应用调稳了,再切回块设备镜像做最终验证。