之前自己编译 QEMU 跑模拟器/虚拟机基本都是跑的一个基于 buildroot 构建的小型根文件系统,虽然也能够完成一些基础的测试,但是功能完备性上相比发行版来说还是要差很多,而且如果想添加新的软件环境也比较麻烦。因此这两天花了点时间倒腾了一下环境,把 QEMU 安装和运行发行版的流程走通,便于后续的学习和研究。

安装系统

发行版我选择的是 Ubuntu,没有什么别的原因,只是因为用的最多。而已经安装好 Ubuntu 系统的磁盘镜像可能不太好找(官网只看到 RISC-V 架构有 pre-installed 版本),因此我选择下载 iso 镜像,然后手动安装。这里可以直接选择通过清华镜像源下载:

wget https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/noble/ubuntu-24.04.3-live-server-amd64.iso

我下载的是较新的版本,因此 seabios 可能不支持,需要更现代的固件,虚拟机场景下通常是 OVMF,使用包管理工具安装一下:

sudo apt install ovmf

找到安装好的 OVMF 固件,通常在目录 /usr/share/OVMF 目录下。

最后创建一个 QEMU 的虚拟磁盘文件 QCOW2(大小自行选择,我的选择为 20G),用作虚拟机的磁盘,它相比传统文件类型的优势在于可以动态扩容。

qemu-img create -f qcow2 ubuntu-vm.qcow2 20G

最后使用下面所示的 QEMU 启动参数进行启动:

qemu-system-x86_64 \
    -machine q35,accel=kvm \
    -cpu host \
    -m 8G \
    -smp 4 \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
    -drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_VARS_4M.fd \
    -cdrom ubuntu-24.04.3-live-server-amd64.iso \
    -boot order=d \
    -hda ubuntu-vm.qcow2 \
    -netdev user,id=net0,hostfwd=tcp::2222-:22 \
    -device e1000,netdev=net0 \
    -serial mon:stdio \
    -display none

其中的 -cdrom 参数相当于将装有操作系统镜像的光盘插入我们的虚拟机器 q35 上,-boot order=d 相当于系统上电进行引导时最先检查的存储设备为光盘,这个过程和我们现实中为一台裸机电脑安装操作系统如出一辙。

由于我的本机 Linux 环境为 WSL2,因此选择纯命令行安装的方式,需要进行下列额外操作将安装器的输出显示在串口:

  • 启动后会出现 GRUB 菜单,选中 Try or Install Ubuntu Server,按 e 进入编辑模式。
  • 找到以 linux 开头的那一行,在末尾加上 console=ttyS0,如 linux /casper/vmlinuz --- console=ttyS0
  • 按 Ctrl+X 或 F10 启动。

运行系统

具体安装过程中的选项在此就不过多介绍了,安装完成后,下次启动时可以去掉 cdromboot 参数:

qemu-system-x86_64 \
    -machine q35,accel=kvm \
    -cpu host \
    -m 8G \
    -smp 4 \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
    -drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_VARS_4M.fd \
    -hda ubuntu-vm.qcow2 \
    -netdev user,id=net0,hostfwd=tcp::2222-:22 \
    -device e1000,netdev=net0 \
    -serial mon:stdio \
    -display none

再次观察上述 QEMU 启动参数,我使用 -netdev user 指定使用用户模式网络,虚拟机能够直接通过宿主机来访问互联网。同时还通过 hostfwd=tcp::2222-:22 设置了端口转发,把宿主机的 TCP 2222 端口转发到虚拟机的 22 端口,宿主机可以通过 ssh 工具连接到虚拟机:

ssh -p 2222 <用户名>@localhost

同理,宿主机要想传输文件到虚拟机,也可以通过:

scp -P 2222 <要传输的文件> <用户名>@localhost:<目录>

而虚拟机要想传输文件回宿主机,可以先为宿主机启用 ssh 服务(如果没有启用的话):

sudo apt update

sudo apt install openssh-server

# 启用 ssh 服务
sudo systemctl start ssh

# 设置开机自动启动
sudo systemctl enable ssh

# 检查 ssh 服务运行状态
sudo systemctl status ssh

此后,虚拟机也可以直接通过 scp 向宿主机进行文件传输(无 -P 2222):

scp <要传输的文件> <用户名>@10.0.2.2:<目录>

使用自己编译的内核

如果只是进行应用程序的开发和测试的话,想必上面的环境已经够用了。但我毕竟是做操作系统相关工作的,难免要对内核代码进行修改并编译测试,因此这就涉及到另一个问题:如何在保留发行版丰富的开发环境的同时,使用自己编译的内核?这一块踩了不少坑,下面直接介绍完整流程:

首先需要编译自己的内核,得到 bzImage,这一块想必不用再介绍了。

然后是需要在内核源代码目录下生成和内核版本对应的 initramfs,得到文件 initrd.img-5.10.0

(py312) lordaeronesz@Snow:~/LWS/510-linux$ sudo mkinitramfs -o ./initrd.img-5.10.0 5.10.0

W: Kernel configuration /boot/config-5.10.0 is missing, cannot check for zstd compression support (CONFIG_RD_ZSTD)
W: missing /lib/modules/5.10.0
W: Ensure all necessary drivers are built into the linux image!
depmod: ERROR: could not open directory /lib/modules/5.10.0: No such file or directory
depmod: FATAL: could not search modules: No such file or directory
/usr/sbin/mkinitramfs: 136: linux-version: not found
I: The initramfs will attempt to resume from /dev/sdb
I: (UUID=f10066eb-414e-4970-a3c9-9a28c963f849)
I: Set the RESUME variable to override this.
cat: /var/tmp/mkinitramfs_mpVlsQ/lib/modules/5.10.0/modules.builtin: No such file or directory
depmod: WARNING: could not open modules.order at /var/tmp/mkinitramfs_mpVlsQ/lib/modules/5.10.0: No such file or directory
depmod: WARNING: could not open modules.builtin at /var/tmp/mkinitramfs_mpVlsQ/lib/modules/5.10.0: No such file or directory
depmod: WARNING: could not open modules.builtin.modinfo at /var/tmp/mkinitramfs_mpVlsQ/lib/modules/5.10.0: No such file or directory

看到上面这么多警告信息我还以为失败了,似乎是因为没有指定内核模块的缘故,这一块先不琢磨了,能得到 initrd 就行。

由于后面要在 QEMU 启动参数中添加传递给内核的 cmdline,因此我们必须确定该发行版下根目录对应的是哪个设备目录,这里要先用 运行系统 章节介绍的启动参数启动 Ubuntu 自带的内核,查看 df -h

lordaeronesz@flame:~$ df -h

Filesystem                         Size  Used Avail Use% Mounted on
tmpfs                              794M 1004K  793M   1% /run
efivarfs                           256K  110K  142K  44% /sys/firmware/efi/efivars
/dev/mapper/ubuntu--vg-ubuntu--lv  9.8G  5.5G  3.8G  60% /
tmpfs                              3.9G     0  3.9G   0% /dev/shm
tmpfs                              5.0M     0  5.0M   0% /run/lock
tmpfs                              3.9G     0  3.9G   0% /run/qemu
/dev/sda2                          1.7G  101M  1.5G   7% /boot
/dev/sda1                          952M  6.2M  945M   1% /boot/efi
tmpfs                              794M   16K  794M   1% /run/user/1000

可以看到,根目录的对应的设备目录为 /dev/mapper/ubuntu--vg-ubuntu--lv,之所这么奇怪而不是类似 sda 这种似乎是使用了 LVM(Logical Volume Manager) 的缘故。

最后,在 QEMU 启动参数中指定我们上面得到的 initrd、kernel、root。完整的启动参数如下:

qemu-system-x86_64 \
    -machine q35,accel=kvm \
    -cpu host \
    -m 8G \
    -smp 4 \
    -kernel bzImage \
    -initrd initrd.img-5.10.0 \
    -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
    -drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_VARS_4M.fd \
    -append "root=/dev/mapper/ubuntu--vg-ubuntu--lv console=ttyS0" \
    -hda ubuntu-vm.qcow2 \
    -netdev user,id=net0,hostfwd=tcp::2222-:22 \
    -device e1000,netdev=net0 \
    -serial mon:stdio \
    -display none

成功启动后,通过 uname -a 查看内核信息:

lordaeronesz@flame:~$ uname -a

Linux flame 5.10.0+ #58 SMP Thu Oct 30 19:18:14 CST 2025 x86_64 x86_64 x86_64 GNU/Linux

可以看到,内核为我自行编译的 5.10 版本,而非自带的 6.8 版本:

lordaeronesz@flame:~$ uname -a

Linux flame 6.8.0-87-generic #88-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 09:28:41 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

说实话,我对操作系统启动这一块的内容不太熟悉,对 initrd、initramfs 等这些名词有些一知半解,所以这一块的配置才踩了那么多坑🥲。在此立个 flag,这部分内容等后续找个时间深入研究一下,再整理成博客。