QEMU,搞嵌入式开发的朋友一定不陌生。最近很多技术群里都在讨论它,说它是 Linux 利器一点不夸张。它是一款知名且开源的模拟器(官网: https://www.qemu.org/ ),可以在 X86 PC 上运行(其实它也能在 Arm 开发板上运行,今天先不讨论这种场景),能够模拟 Arm、MIPS、RISC-V 等各种 CPU 和开发板,以及网卡、声卡、键盘、 sdcard、emmc、usb 等外设。
你可以把它当作一块召之即来的开发板,在上面运行 U-Boot、Linux Kernel,甚至 Ubuntu 等各种软件和操作系统。
有时候想体验一下 mainline 上最新的 U-Boot 或者 Linux Kernel,却发现手边没有合适的板子,或者手上的板子搭载的 U-Boot 和 Linux Kernel 版本都比较低。这时候 QEMU 就能帮你迅速实现这个愿望。
接下来我们一起感受一下 QEMU 的强大和乐趣:手把手教你在 Ubuntu 系统中通过 QEMU 运行基于 Arm CPU 的 Linux 系统。
QEMU 安装
我们需要安装的是 Arm 版本的 QEMU。如果直接在 Ubuntu 上用 sudo apt install qemu-system-arm 命令安装,得到的 QEMU 版本比较旧,最好通过源码编译。
我在 Ubuntu 18.04 系统上发现,系统默认安装的 QEMU 在图形模式(不带 -nographic 参数)下无法启动。

参考上面步骤编译成功后,会得到:qemu-system-arm 和 qemu-system-aarch64。
前者用来模拟 32 位的 Arm CPU,比如 Arm9 / Arm11、 Cortex-A7 / A9 / A15。后者用来模拟 64 位的 Arm CPU,比如 Arm Cortex A53、A57。
可以用 qemu-system-arm -machine help 命令查看所支持的开发板:

是不是有很多熟悉的开发板都在里面?i.MX、EXYNOS 这些知名芯片都有包含。
这里我们使用 vexpress-a9 这款开发板。vexpress-a9 是 Arm 公司自己设计的一款 4 核 Cortex-A9 开发板,U-Boot、Linux Kernel 和 QEMU 对这款开发板都做了完整的支持。
编译 U-Boot
第一步:U-Boot 代码下载
git clone https://gitlab.denx.de/u-boot/u-boot.git
下载完成后,可以看到 configs 目录下有针对这款开发板的配置文件:
vexpress_ca9x4_defconfig

第二步:编译
make vexpress_ca9x4_defconfig
make CROSS_COMPILE=arm-linux-gnueabihf- all
最终编译生成 elf 格式的可执行文件 u-boot 和纯二进制文件 u-boot.bin,其中 QEMU 可以启动的是 elf 格式的可执行文件 u-boot。

编译 Buildroot
启动一个 Arm Linux 系统,一般都需要三件套:Bootloader、Linux Kernel、rootfs(根文件系统)。
在很久以前,制作 rootfs 是一件很麻烦的事情:交叉编译 busybox,然后手动建立标准的 Linux 系统目录,再把编译 busybox 生成的各种文件和库拷贝过来。如果还需要其他模块,再交叉编译。如果交叉编译的某个模块依赖其他库,还得想办法解决依赖关系。最后还要手动建立设备节点,设置对应权限。一步一步做下来,任意一个环节出错,启动时就会遇到莫名其妙的问题。
Buildroot 项目出现之后,如同它的 Slogan:Making Embedded Linux Easy,构建 rootfs 就轻松多了。用一个群友的话说:
“自从用了 buildroot,我就告别了刀耕火种的野蛮生活。那种文件系统自己定制,需要任何工具都要自己下源码交叉编译然后被各种库问题搞的焦头烂额的时代一去不复返了。”

◆ 代码下载
git clone git://git.buildroot.net/buildroot
Buildroot 代码仓库默认只包含一个编译框架,所以代码量很小,下载很快。真正构建 rootfs 需要的各种代码包,是根据你的配置选项在编译时才下载的。
◆ 配置
Buildroot 提供了和 U-Boot、Linux Kernel 等主流开源项目一样的 menuconfig 配置接口,可以通过 make help 来查询支持的各种命令:

开发者只要执行 make menuconfig 命令,就能通过这个熟悉的界面选择自己需要的组件,定制自己的 rootfs。
首先要配置的是 Target options 选项:

大部分 Arm 都是小端模式,所以选上 little endian。这款开发板的 CPU 是 Cortex-A9。我们将使用 Linaro GCC 进行编译,Linaro 的 GCC 默认都打开了 hardfloat 的支持,所以选上 VFP extension 和 EABIhf。
交叉编译工具 Linaro GCC 的安装可以参考之前的文章:《一次搞定 Arm Linux 交叉编译》。
Build options 选项:

第一个选项是设置最后生成的配置文件的保存路径。buildroot 可以针对不同的板子生成特定的 defconfig 文件,默认保存在 configs 目录下。
自己修改各项配置后,执行 make savedefconfig 命令,就会生成新的 defconfig 文件:

下次编译之前,可以直接执行 make ca9_mini_defconfig 命令来加载已有的配置。
第二个选项设置 buildroot 下载的各种第三方包的存储路径,默认在 dl 目录下:

设置 Toolchain:

因为这里使用电脑上自己安装的 toolchain,所以我们选择 External toolchain 和 Custom toolchain。然后在 Toolchain path 中填写 toolchain 在电脑上安装的位置。如果不知道具体位置,用 which 命令查看:

另外要注意 Toolchain prefix 这个前缀别写错。
设置 toolchain 的版本和用来编译这个 toolchain 的内核头文件的内核版本。Toolchain 版本根据 Toolchain 的名字或者通过 arm-linux-gnueabihf-gcc -v 命令就能查到。
编译 Toolchain 的内核头文件对应的内核版本是什么呢?在这个选项上敲 h 键,会看到下面的帮助选项:

原来这个版本可以在 toolchain 里面的 version.h 这个文件查到。打开这个文件:
arm-linux-gnueabihf/libc/usr/include/linux/version.h

263680 对应的十六进制为 0x40600,右移 16 位,得到的版本号为 4。这就是上面 4.0.x 的由来。
这里也教给大家一个小窍门:在 make menuconfig 做配置的时候,如果遇到看不懂的选项,直接在这个选项上敲 h 键,会看到有用的帮助信息,对这个选项做进一步解释。
因为 vexpress_a9 内核启动的控制台名字叫 ttyAMA0,所以我们还要在 System configuration -> Run a getty (login prompt) after boot 选项中配置 TTY Port 为 ttyAMA0。否则文件系统挂载后无法进入控制台。

我们把编译的 rootfs 以 initramfs 的形式和 Linux Kernel 链接在一起。为了让根文件系统镜像尽量小,可以对文件系统采用 lz4 压缩,所以 Filesystem images 还要做如下配置:

到这里,一个最精简的 buildroot 已经配置完成。如果还需要其他命令或者工具,可以在 Target Packages 下面开启:

如果你需要的某个模块 buildroot 里面没有,还可以自己添加:

比如加入上面这个补丁,就可以让 buildroot 在编译的时候自动下载 https://github.com/rockchip-linux/io.git 并编译。
退出后执行 make 命令开始编译。Buildroot 编译过程中会自动通过网络下载需要的各种包,所以要保证网络畅通。编译完成后的输出信息如下图所示:

编译 Linux Kernel
1)代码下载
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
Linux mainline 已经有了对 vexpress_a9 这块板子的支持:

这里面的 ca 就是 Cortex-a 的简写,所以 ca9 就是 Cortex-A9,ca15 就是 Cortex-A15。
2)配置
把前面 buildroot 编译的 rootfs.cpio.lz4 拷贝到 linux kernel 根目录下:
cp ../buildroot/output/images/rootfs.cpio.lz4 .
make ARCH=arm vexpress_defconfig
执行 make ARCH=arm menuconfig 命令,我们修改一些基本的配置:

在 General setup -> Initramfs source file 处填写 rootfs.cpio.lz4,就是我们前面从 Buildroot 拷贝过来的 rootfs。这里我们把它和内核编译在一起。当然,rootfs 也可以单独作为一个文件,放在独立的分区去加载,这种方式可以留到以后尝试。

在 Kernel hacking -> printk and dmesg options 选项中选中第一项,这样打印的内核 log 前面会附带时间戳信息,比较直观。
退出、保存,然后编译:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

编译完成。如果在编译 Linux kernel 或者 u-boot 的过程中遇到什么错误,可以参考之前的文章:《Linux Kernel 和 U-Boot 编译的那些事》。
启动 QEMU
前面说过,QEMU 可以模拟 sd 卡等外设。我们就把编译好的固件放在一个模拟的 sdcard 上,让 QEMU 从这张模拟的 sd 卡启动 Linux 系统:
◆ 制作 sd 卡镜像,并将它格式化成 fat 格式
dd if=/dev/zero of=sd.img bs=4096 count=4096
mkfs.vfat sd.img
◆ 把编译好的 kernel zImage 和 dtb 文件拷贝到 sd.img 中
sudo mount sd.img /mnt/ -o loop,rw
sudo cp arch/arm/boot/zImage /mnt/
sudo cp arch/arm/boot/dts/vexpress-v2p-ca9.dtb /mnt/
sudo umount /mnt
◆ 启动 QEMU
sudo qemu-system-arm -M vexpress-a9 -m 512M -kernel ../uboot-imx/u-boot-nographic -sd sd.img

-M 参数指定启动的板子为 vexpress-a9。-m 指定这块板子的内存为 512MB。-kernel 指定 QEMU 启动时首先执行的程序,这里指定为前面编译好的 u-boot 可执行文件。-sd 参数指定前面制作的 sd.img。因为这里是从命令行启动,所以加了 nographic 参数。
可以看到 u-boot 已经顺利启动并进入命令,下面我们来启动 Linux Kernel。
◆ 首先通过 fatload 命令把 sd.img 里面的 zImage 和 dtb 文件读到开发板的内存中
fatload mmc 0:0 0x62008000 zImage
fatload mmc 0:0 0x64008000 vexpress-v2p-ca9.dtb
这里的 0x62008000 和 0x64008000 分别对应 zImage 和 dtb 文件在内存中的加载地址。这两个地址是怎么来的呢?
在 Linux Kernel 中,有如下定义:

这个 textofs 定义的是 Linux kernel zImage 执行地址对应的内存偏移地址,默认偏移为 0x8000。
在 u-boot 命令行中输入 bdinfo 命令,可以查到这块开发板内存的起始地址:

可以看到这块开发板的内存起始地址为 0x60000000,所以对应内核的起始地址为:0x62008000。
dtb 的加载地址没有特别要求,注意和 Linux Kernel Image 避开、不要重叠即可。
◆ 通过 bootz 命令启动 Linux Kernel
bootz 0x62008000 - 0x64008000


启动到最后输入 root 就可以进入命令行控制台。到这里,我们就顺利地在 QEMU 上把 Arm Linux 运行起来了。
QEMU 能否完全替代开发板?
QEMU 是一个模拟器,它和真正的开发板还是有一定区别。
它无法完全模拟真实的硬件行为,也很难模拟一个 gpio 让你去拉高拉低或去点一个 led 灯,又或者去模拟 dram、一个 LCD 接口,让你去接一块显示屏……看起来它不能模拟的东西太多了,那它到底有什么用呢?
它的强项是模拟那些不涉及具体硬件外设的场景,比如:
- 你想快速体验一下最新的 u-boot 和 linux kernel,它拿过来就能跑。
- 你想在 Arm 上运行 Ubuntu、Debian 这些流行的 Linux 发行版,用它就行。
- 你想研究 u-boot 或者 linux kernel 的启动流程,它也很合适。我有时候在具体的开发板上移植 Linux 内核,发现某个流程跑得很异常,又不确定正常流程是什么样的时候,就直接拿 QEMU 来跑一下做对比。
- 你想了解文件系统的挂载过程,它很合适。
- 你想学习 Arm 汇编,完全可以在 QEMU 上跑,配合 GDB 就能单步调试。
- 你想学习 Linux 设备树,它可以拿来做实验。
- 再或者你想编译某个开源项目在 Arm 开发板上运行,而交叉编译比较困难,就可以考虑直接在 QEMU 上运行一个 Arm Ubuntu 来编译,也许会简单很多。
假设你以前一直玩单片机、做硬件,也想体验一下 Arm Linux 开发,用 QEMU 试试再适合不过了。
用在对的场景,QEMU 就是一款召之即来的利器!