工欲善其事,必先利其器。qemu 作为嵌入式开发中的常用工具,可以方便的模拟不同的硬件开发环境,方便我们做早期代码调试和功能验证。
qemu 是什么?
QEMU 是一个开源的虚拟机仿真器,它能够在多种架构上模拟计算机硬件,并在其上运行各种操作系统。它可以用于虚拟化、调试和测试目的。QEMU 支持许多不同的处理器架构,包括 x86、ARM、MIPS 等,因此可以用于模拟运行各种操作系统,如 Linux、Windows、macOS 等。
为何要用 qemu?
- 免费,开源,社区活跃,通用性好。
- 支持丰富的配置选项,可以模拟大量的外设,甚至可以自己开发相应的模块加入仿真。
- 提前做原型验证。在嵌入式开发过程中,比如开发驱动,可以先在 qemu 仿真平台验证驱动程序的基本功能,速度快,调试方便。qemu 平台验证通过后再去实际的硬件平台调试,往往事半功倍。
- 便于学习和研究。很多想学习嵌入式的同学,如果直接上手买一套硬件的开发板和仿真器,价格并不便宜。使用 qemu 仿真可以在前期很好的帮助我们入门,等到掌握了一定的基础就可以实际在硬件平台上手了。如果只是简单的跑跑小程序玩一玩,qemu 仿真足够应付。
创建属于自己的 qemu 虚拟仿真环境
说了这么多好处,但其实比起大家熟知的 VMware、virtualbox 这类虚拟机,qemu 最大的缺点就是上手有一定的门槛,初次接触往往不知从何处入手,面对大量的选项,也不知哪些该用哪些不该用,要怎么配置,一头雾水。 使用 qemu 推荐从命令行启动。相比于图形化的前端,命令行启动有如下优势:
- 灵活:允许你添加各种定制化的选项来丰富仿真环境的功能。
- 资源占用少:相比于 GUI 图形界面,命令行启动占用的系统资源更少,运行更流畅。同等条件下,甚至可以在同一个系统上运行多个 qemu 虚拟机。
- 一致性好:命令行参数的配置可以保证在不同的平台上实现功能的一致性。 本文以在 x86 机器上使用 qemu 仿真 ARM64 平台为例,教大家如何一步一步从零搭建属于自己的 qemu 仿真环境。如果要仿真其他的平台,可以以此为参照,举一反三。
- 推荐在 Linux 系统上安装 qemu。可以登录www.qemu.org/download/ 下载最新的qemu版本。根据需要选择源码构建安装或者直接使用各个Linux发行版仓库中的版本。如无特殊需要,可以直接使用Linux发行版自带的qemu,安装简单方便。检查qemu是否安装成功:
$ qemu-system-aarch64 --version
QEMU emulator version 8.2.1
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers
当前我的环境上安装的是 qemu 8.2.1,linux 版本是 Ubuntu 22.04
- 下载 Linux 内核代码并编译镜像
(1)以下载6.7.6版本的Linux kernel代码为例,其他版本可自行修改
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.7.6.tar.xz
(2)解压代码压缩包
$ tar -Jxf linux-6.7.6.tar.xz
(3)安装编译工具和依赖库
$ sudo apt install gcc-aarch64-linux-gnu
$ sudo apt install libncurses5-dev build-essential git bison flex libssl-dev
检查安装情况
$aarch64-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=aarch64-linux-gnu-gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/aarch64-linux-gnu/11/lto-wrapper
Target: aarch64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --without-target-system-zlib --enable-multiarch --enable-fix-cortex-a53-843419 --disable-werror --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=aarch64-linux-gnu --program-prefix=aarch64-linux-gnu- --includedir=/usr/aarch64-linux-gnu/include --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
(4)指定环境变量
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-linux-gnu-
(5) 配置内核:这里采用ARM公司提供的Versatile Express开发平台模拟
cd 到解压出来的内核代码根目录下
$ cp arch/arm/configs/vexpress_defconfig .config
$ make menuconfig
添加hotplug的支持
Device Drivers
-> Generic Driver Options
-> Support for uevent helper
(/sbin/hotplug) path to uevent helper
配置内核的虚拟地址空间范围
Kernel Features --->
Page size(4KB) --->
Virtual address space size(48-bit)--->
增加debug_fs的支持
Kernel hacking --->
Generic Kernel Debugging Instruments --->
[*] Debug Filesystem
(6)编译内核,编译产物为arch/arm64/boot/Image。
$ make all
视自己的机器配置情况,为了提升编译速度,可以使用make all -j4 或者make all -j8
- 制作根文件系统 rootfs
(1)下载busybox
$ wget http://busybox.net/downloads/
$ tar jxvf busybox-1.36.1.tar.bz2
(2) busybox编译配置打开静态链接选项
$ make menuconfig
Settings --->
[*] Build static binary (no shared libs)
(3) 编译busybox,执行成功会在代码根目录生成_install目录
cd 到busybox代码根目录
$ make && make install
(4) 在rootfs里添加/etc /lib 和/dev目录
$ cd _install
$ mkdir etc lib dev
$ ls
bin dev etc lib linuxrc sbin usr
// 在etc目录下创建如下文件
$ cat profile
#!/bin/sh
export HOSTNAME=virt-machine
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
$ cat fstab
#device mount-point type options dump fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0
$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
// 指明挂载的文件系统
$ mkdir -p etc/init.d
$ cat etc/init.d/rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
$ chmod +x rcS
// 添加设备文件
$ cd dev
$ sudo mknod console c 5 1
$ sudo mknod null c 1 3
// 拷贝lib库到lib目录,方便qemu虚拟机启动后,动态编译的程序可以运行
$ cd linb && cp /usr/aarch64-linux-gnu/lib/*.so* -a .
$ ls
ld-linux-aarch64.so.1 libatomic.so.1 libgcc_s.so.1 liblsan.so.0 libnss_compat.so.2 libresolv.so libtsan.so.0
libBrokenLocale.so libatomic.so.1.2.0 libgomp.so.1 liblsan.so.0.0.0 libnss_dns.so.2 libresolv.so.2 libtsan.so.0.0.0
libBrokenLocale.so.1 libc.so libgomp.so.1.0.0 libm.so libnss_files.so.2 librt.so.1 libubsan.so.1
libanl.so libc.so.6 libhwasan.so.0 libm.so.6 libnss_hesiod.so libstdc++.so.6 libubsan.so.1.0.0
libanl.so.1 libc_malloc_debug.so libhwasan.so.0.0.0 libmemusage.so libnss_hesiod.so.2 libstdc++.so.6.0.30 libutil.so.1
libasan.so.6 libc_malloc_debug.so.0 libitm.so.1 libnsl.so.1 libpcprofile.so libthread_db.so
libasan.so.6.0.0 libdl.so.2 libitm.so.1.0.0 libnss_compat.so libpthread.so.0 libthread_db.so.1
- 制作虚拟硬盘
cd 到内核源码根目录下,执行下面的命令
dd if=/dev/zero of=rootfs_ext4.img bs=1M count=1024
mkfs.ext4 rootfs_ext4.img
mkdir -p tmpfs
sudo mount -t ext4 rootfs_ext4.img tmpfs/ -o loop
sudo cp ~/busybox-1.36.1/_install/ ./rootfs -a
sudo chown -R root:root rootfs
sudo cp -af rootfs/* tmpfs/
sudo umount tmpfs
chmod 777 rootfs_ext4.img
- qemu 启动
// 创建qemu和主机的共享目录,该目录下的文件可以同时被qemu虚拟机中的Linux和主机上的Linux访问修改
mkdir kmodules
// 启动qemu虚拟机
qemu-system-aarch64 -machine virt -cpu cortex-a57 \
-m 1024 \
-smp 4 \
-kernel arch/arm64/boot/Image \
--append "noinitrd root=/dev/vda rw console=ttyAMA0 loglevel=8" \
-nographic \
-drive if=none,file=rootfs_ext4.img,id=hd0 \
-device virtio-blk-device,drive=hd0 \
--fsdev local,id=kmod_dev,path=$PWD/kmodules,security_model=none \
-device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount
这里我们逐行解析下 qemu 的启动参数:
-
qemu-system-aarch64: 这是 QEMU 的命令行工具,用于启动模拟 ARM64 架构的虚拟机。 -
-machine virt: 这个选项指定要模拟的虚拟机的类型,即通用的虚拟机。 -
-cpu cortex-a57: 指定虚拟机使用的 CPU 模型,这里选择了 Cortex-A57。 -
-m 1024: 设置虚拟机的内存大小为 1GB。 -
-smp 4: 指定虚拟机的 CPU 核心数量为 4 个。 -
-kernel arch/arm64/boot/Image: 指定用作虚拟机启动镜像的内核文件。 -
--append "noinitrd root=/dev/vda rw console=ttyAMA0 loglevel=8": 这个参数指定了启动内核时要传递给内核的命令行参数。具体来说:noinitrd: 表示不使用 initramfs(即不使用 RAM 文件系统)。root=/dev/vda: 指定根文件系统的设备为/dev/vda。rw: 将根文件系统挂载为读写模式。console=ttyAMA0: 将控制台输出重定向到串行端口 ttyAMA0。loglevel=8: 设置内核日志级别为 8,以便显示更多的调试信息。
-
-nographic: 这个选项告诉 QEMU 不要显示图形窗口,而是将所有输出发送到控制台。 -
-drive if=none,file=rootfs_ext4.img,id=hd0: 这个选项定义了一个虚拟硬盘驱动器。具体来说:if=none: 指定不使用任何接口类型,因为后面会使用virtio-blk-device设备来连接这个虚拟硬盘。file=rootfs_ext4.img: 指定虚拟硬盘的镜像文件为rootfs_ext4.img。id=hd0: 为这个虚拟硬盘指定一个唯一的标识符。
-
-device virtio-blk-device,drive=hd0: 这个选项定义了一个 virtio 块设备,连接到之前定义的虚拟硬盘。drive=hd0指定连接到名为hd0的虚拟硬盘。 -
--fsdev local,id=kmod_dev,path=$PWD/kmodules,security_model=none: 这个参数定义了一个本地文件系统设备,用于将主机上的文件系统挂载到虚拟机中。具体来说:local: 指定这是一个本地文件系统。id=kmod_dev: 为这个设备指定一个唯一的标识符。path=$PWD/kmodules: 指定要挂载到虚拟机中的文件系统路径。这里假设在当前工作目录下有一个名为kmodules的文件夹。security_model=none: 禁用安全模型,允许对文件系统进行完全的读写访问。
-
-device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount: 这个选项定义了一个 virtio-9p 设备,用于将之前定义的本地文件系统挂载到虚拟机中。具体来说:fsdev=kmod_dev: 指定要挂载的文件系统设备。mount_tag=kmod_mount: 指定挂载的标签名称,在虚拟机中将使用这个标签来识别挂载的文件系统。
总结
本文简单介绍了如何使用 qemu 在 x86 平台上搭建一个 arm64 的仿真环境,方便用户开发和验证在 arm 平台上的应用程序。当然,qemu 的功能包罗万象,本文只是介绍了其中很小的一部分。万变不离其宗,读者可以根据自己的需要,制作特定的根文件系统 rootfs,自行裁剪 busybox,使用不同的命令行参数启动虚拟机。
关注微信公众号 【漫谈编程】,获取更多有用干货!