在QEMU中加载自己的 Linux Kernel
文章目录
背景
最近的工作中涉及到一些需要魔改 Linux Kernel 代码和加载一些自己的 kernel module 的工作。 之前每次编译完 kernel 以后,都会把新的 kernel 装到自己在云上的开发机里,每次都很是担惊受怕。 每次都要“关机-备份-安装-重启”。前几天还因为一些奇怪行为,直接把文件系统给玩坏了,导致丢了几天的草稿代码。
之后就一直想着,是不是应该把我的魔改 kernel 放到一个相对隔离起来的环境里。 至少玩坏了以后,不要把我的主力开发机反复弄死。所以就研究了一下怎么用 QEMU 来加载我自己的 kernel。
QEMU 很贴心的提供直接从 bzImage 启动的功能: Direct Linux Boot
但是要直接从bzImage启动,QEMU还需要用户提供一个-hda
或者-initrd
。
这篇文章主要包含以下几个部分:
- 安装QEMU
- 编译 bzImage
- 准备一个基于 Arch Linux 的磁盘映像
- QEMU,启动!
安装 QEMU
我的系统是跑在 WSL 里面的 Ubuntu。安装直接 apt 就完事了。
|
|
因为我暂时不太关心别的架构(譬如说ARM64)的情况,所以只装了-x86
的版本。如果需要别的 arch 支持,
装别的版本就可以了。
也有人推荐下载最新的源码,从头编译。(你都编译一遍 kernel 了,再编译一个 QEMU 算什么) 只能说开心就好。
编译 Kernel
编译 Kernel 的几个步骤也很简单:
- 下载代码
- 准备编译依赖
- 准备config
- 开始编译
下载 Kernel 代码
Linux Kernel 的代码可以从 www.kernel.org 下载,也可以从 GitHub 下载。 地址分别是: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ 或者 https://github.com/torvalds/linux
|
|
不要问什么为什么用git下载,不直接用tar包。问就是不会用tar1。
安装依赖
|
|
具体每个依赖是做什么的,参考 https://docs.kernel.org/process/changes.html, 根据config的不同,不一定都用得上。
特别说一下 ncurses
。 编译 Kernel 的时候不需要。但是如果跑 menuconfig
2 的话,会依赖这个。
准备config
一般 distro 都会把当前 Kernel 在编译时用的 .config
放到 /boot/
下面。
但是因为 WSL 不是一个标准的 Linux,他的 /boot 下面是空的。
所以比较方便的方式还是从网上找一个别的 distro 正在用的 config。
譬如说 WSL 的 config 在这里: https://github.com/microsoft/WSL2-Linux-Kernel/blob/linux-msft-wsl-5.15.y/arch/x86/configs/config-wsl 。
而 Arch Linux 的 config 在这里: https://gitlab.archlinux.org/archlinux/packaging/packages/linux/-/blob/main/config
不过不要用 WSL 的config,会变得不幸。几乎什么驱动都没有配置。
我选的是我们隔壁 Azure Linux 团队在维护的一个config: https://github.com/microsoft/CBL-Mariner/blob/2.0/SPECS/kernel/config
|
|
编译内核
|
|
这里的 O=...
可以参考 https://docs.kernel.org/kbuild/kbuild.html
menuconfig
可以参考 https://docs.kernel.org/kbuild/kconfig.html#menuconfig
在 menuconfig 的时候,记得把 E1000 的驱动选上,后面会用。
编译成功以后,会在输出的最后面看到一行类似这样的输出
|
|
这个bzImage
就是 QEMU 需要的内核文件
准备磁盘映像文件
这里的内容主要修改自 Stefan Koch 的 https://blog.stefan-koch.name/2020/05/31/automation-archlinux-qemu-installation
主要的区别是在 Stefan 的文章中,他的目标是准备一个完整的磁盘文件,然后直接从磁盘启动。 这里就涉及了 syslinux, initramfs 等和 boot loader 相关的东西。 因为我们是用 QEMU 直接启动 kernel,这些东西都不需要了。
主要步骤包括
【补课】Linux 启动流程
对于 Kernel 而言,除了“操作系统”课程中会讲的最基本的内存管理,进程管理,各种设备的驱动 (和其他内核黑科技,譬如eBPF)之外,几乎用户日常会用的东西都没有。 无论是 bash、systemd 或者 glibc,所有这些运行在用户态的东西,都是 kernel 之外,需要用户(或者说 distro)来提供的。
Kernel 在完成自己的启动以后,会把系统的控制权交给 init 程序,来完成剩下的用户态的工作。
在 Kernel 启动以前,加载 Kernel 的工作则是由 boot loader 来完成的。 相关的关键词包括了 UEFI,grub,security boot 等等。
而 QEMU 的 Direct Linux Boot 某种意义上就是起到了 boot loader 的作用,直接加载内核和磁盘。
在 How Linux Works 5 这本书的 “Chapter 5: How the Linux Kernel Boots” 中非常详细的讲述了 Linux 的启动流程。
准备磁盘 Raw Image
|
|
qemu-img
创建的是一个空的磁盘文件。然后通过 losetup
把文件模拟成一个 block device。
最后用 parted
和 mkfs
来格式化这个虚拟的磁盘。
arch-chroot
|
|
这样就把 bootstrap 包加载 /tmp/arch
下面。但是在执行arch-chroot
之前,还有一件重要的事情。
因为 bootstrap 的环境里几乎什么工具都没有,主要是没有编辑器,而我又不太会用sed。
所以需要先用当前系统的编辑器改一下 pacman 的 mirror list。至于具体选哪个mirror,就是个人偏好的问题了。
|
|
在这之后就是相对标准的 Arch Linux 安装流程了,相关内容也可以参考 https://wiki.archlinux.org/title/Install_Arch_Linux_from_existing_Linux
|
|
因为我们这里不需要系统内核,所以只需要装base
和 linux-firmware
包,
而不需要 linux
包。
至于 dhcpd
和 openssh
则是为了后面 QEMU 启动起来以后,方便从 Host 机器通过 ssh
接入到 Guest 机器里面。不需要抱着 QEMU 的 serial console 来执行别的命令。
这个时候如果愿意,也可以装更多的包,譬如vim
, sudo
之类的。不过这些东西,稍后在 QEMU
启动起来以后再安装也没差。
|
|
除了 https://wiki.archlinux.org/title/Installation_guide#Configure_the_system 中提到的几项系统配置以外,主要就是给 root 加密码,不然 serial console 都登陆不进去。 最后添加了新用户,并且打开了 sshd 的允许密码登录的选项。
转换成 qcow2 格式
|
|
最后就是退出 chroot 环境,并且清理资源。然后把 raw image 给转换成 QEMU自己的 qcow2 格式
启动 QEMU
这一部分主要参考了 Chris Gioran 的 https://radiki.dev/posts/qemu-setup-for-kernel-dev-1
我这里的主要区别是,他的文章中讲了如何加载一个 Ubuntu cloudimg 的磁盘,包括如何配置cloud-init。 而我是用的前一步准备的 Arch Linux 磁盘。
我在尝试用 Ubuntu cloudimg 的时候,用的是 22.04 LTS,结果遇到了几个问题。
一个是磁盘映像里的 fstab 会尝试挂载一个 UEFI 分区,这个会导致系统启动不起来。
我后来尝试通过 virt-edit
修改了fstab,注释掉 UEFI 分区以后才能正常启动。
另一个问题是跟cloud-init相关的。Chris 是通过一个iso来初始化用户密码的。 但是我开始的 kernel 编译参数里,没有编译 CDROM 相关的驱动,导致读不了光盘。 重新编译 kernel 以后解决了这个问题。
|
|
-enable-kvm
-cpu host --enable-kvm
这一行不是必选项。主要是利用 kvm 把 Host 机器的 CPU 借给 Guest
去用,而不是用纯粹的软件模拟的CPU。跑起来会更快一点。
-m
-m 2048
就是分配给 Guest 2G 内存。
-nographic
因为系统是跑在命令行里的,不需要 QEMU 的 UI。
-device 和 -netdev
-device e1000,netdev=net0
是给机器加上了一个类型为 e1000 的,名字叫 net0 的网卡。
-netdev user,id=net0,hostfwd=tcp::5555-:22
是说这个 net0 的网卡上的 TCP 22 端口会被映射到
Host 机器的 5555 端口上。
这样机器启动之后,就可以从外面通过 SSH 来使用这个 VM 了。
-kernel 和 -hda
-kernel
和 -hda
分别指向了之前 kernel 文件和磁盘文件。
-append
-append
里的东西,是提供给 kernel 的命令行参数。
完整的说明可以参考 https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html
console=ttyS0
相当于把当前的命令行界面做成了serial console,
这样可以在qemu的命令行界面里看到 kernel 的各种输出信息。
也可以通过 serial console 去登录系统。
init=/sbin/init
就是前面提到的,系统启动后把控制权交给 init 程序,里的init。
在实践中,这里会指向 systemd。但是其实这个 init 可以是任何可执行文件。
Hello-world 也可以,无非就是后面会 panic。
root=/dev/sda1 rw
是说把 /dev/sda1
给 mount 到 /
上面去,并且可读可写。
退出 QEMU
如果一切顺利的话,稍等几秒钟,命令行里刷完了启动信息以后,应该就提示登录了。 用之前的root账号或者新建的账号,都可以从 qemu 的命令行界面里登录进去了。
我之前从外面用 SSH 访问的时候,遇到了一些问题。后来发现是网卡驱动没装。 建议是把 E1000 的相关驱动做成 build-in module,主要是方便。
在测试完 kernel 以后,可能遇到的问题是:怎么从 QEMU 里退出来。
答案是 ctrl-a x
。
更多的操作参考: https://www.qemu.org/docs/master/system/mux-chardev.html 譬如说
ctrl-a c
可以进入 QEMU 的命令模式
不知道是不是我的 QEMU 的版本的问题,在 QEMU 退出以后,我的当前命令行的有些控制字符的处理会有问题。
解决方案是执行一下tset
(注意,不是test)。
更多
用 GDB 调试 kernel
加载基于 Busybox 的 initramfs
https://blog.jm233333.com/linux-kernel/build-and-run-a-tiny-linux-kernel-on-qemu
可是 Arch Linux 不支持 ARM
LFS please. Or try manjaro.
-
How Linux Works, What Every Superuser Should Know by Brian Ward ↩︎
文章作者 srayuws
上次更新 2023-12-21