先把韬哥的分享帖出来
useradd -m glj -s /bin/bash -d /home/glj
前言
这一篇作为开场,主要是介绍最基本的使用方式:基于一个已经完成编译的内核目录,用新生成的内核 image 启动内核,看看是否一切正常。
建议大家可以一边看一边动手跟着把流程走一走,工具嘛,熟悉了就亲切方便了。
关于体验环境,大家可以直接使用自己的机器,安装 qemu 就可以;也可以考虑使用我们的 x86 公共虚拟机环境,我在里面已经布置好了编译目录,以及下文会提到的一些简单的小脚本,让想体验的同事可以直接一键启动。
公共虚拟机环境:
ssh [email protected]
cd /virt-space
注意:不同架构的某些命令会有差别,比如 console 在 x86 是 ttyS0,在 arm 可能是 ttyAMA0 之类的。
qemu 加 kernel 启动
为了方便调整参数,避免每次都要手敲,我把命令放到了 virt-space 的脚本 1-qemu.sh 里,其实就是启动 exec qemu 的单行命令。(大家在 virt-space 里可以直接./1-qemu.sh 启动;如果是在自己的环境里,调整一下 KERNEL 路径即可;)
#!/bin/bash
QEMU=qemu-system-x86_64
KERNEL=4.19-x86/4.19-x86-build/
exec $QEMU \
-enable-kvm \
-m 2048M \
-smp 2 \
-nographic \
-kernel $KERNEL/arch/x86/boot/bzImage \
-append "console=ttyS0 nokaslr" \
$@ \
exit 0
ok,这样我们就完成了基于 image 的虚拟机启动!
在上面的参数里:
前面几个是基本的 cpu/memory 配置,我一般习惯 2 个 cpu+2G 内存,日常使用足够。
kernel 指定 image 路径,append 指定 cmdline,还可以有 initrd。
如果在本机操作的话我一般不加-nogprahic,但如果是 ssh 到 virt-space,因为在 ssh 里没法起图形,
那必须使用 nographic,否则会报错 gtk 初始化失败。
在 qemu 的窗口(图形化,或者 console)里,我们可以看到内核的启动日志输出,最后会因为找不到 initrd 而走到 panic,但整个内核已经正常启动完成了。——如果我们的问题是修改内核后完全起不来(比如修改了 sched/mm 等内核核心模块),那也许这种最基本的方式就足够我们调试排查问题了。
gdb 调试
我们调整一下启动参数,并且启动 qemu,它会卡住等待:
#!/bin/bash
QEMU=qemu-system-x86_64
KERNEL=4.19-x86/4.19-x86-build/
exec $QEMU \
-enable-kvm \
-m 2048M \
-smp 2 \
-nographic \
-kernel $KERNEL/arch/x86/boot/bzImage \
-append "console=ttyS0 nokaslr" \
-s \
-S \
$@ \
exit 0
在另一个窗口,我们执行 gdb:
cd /virt-space/4.19-x86/4.19-x86-build/
gdb vmlinux
(gdb) target remote:1234
(gdb) hb start_kernel
(gdb) c
... -> hit the breakpoint "start_kernel"
(gdb) bt
(gdb) b rest_init
(gdb) b acpi_init
...
(gdb) c
... -> will hit the next breakpoint
这样,我们就成功地开始用 gdb 调试我们新编译的内核了。
说明:
- -s:
更正式的方式是:-gdb tcp::1234。——“-s”就是一个语法糖。(注意,tcp 后面是两个冒号!) qemu 会在该端口起一个服务作为 gdb server;gdb 可以用 target remote 命令连接这个端口。多个虚拟机都用“-s”时会有冲突,所以如果在 virt-space 里,大家还是手动指定自己的 port 吧。
- -S:
这个参数会告诉 qemu,让虚拟机停留在上电的第一条指令等待。所以可以和 gdb 配合:虚拟机在第一条指令等待,gdb 连接上后用“c”继续执行。
- hb:
大家注意,在很多架构里,第一个 start_kernel 断点,必须用硬断点,hardware-breakpoint。等到执行到 start_kernel,被断住时,之后就可以随便用普通的 breakpoint 了。其原因是: b 可以认为是基于指令替换+int3+ptrace 的,所以只有在代码已经被加载到内存里了才能有效替换;当在 S 处停住时,正准备跑 bios,kernel 还没 load,所以 b 可能会无效;而硬件断点呢?它的底下不是指令替换,而是由硬件提供的对某个地址的强制 watch,所以无论在什么时候 hb 都可以有效,也不管这个地址是 bios 还是 grub 还是 kernel 跑到了,都会被断住。——当然它的代价就是需要硬件支持,而且不像普通断点一样可以无限多,一般有硬件数量限制。我们可以搭配使用:S时使用 hb,监控 start_kernel 地址;等撞到了这个,那 kernel 肯定已经被 load 到内存了,就可以放心使用普通断点了。如果大家在实践中发现没有断到,可以如何排查呢?万变不离其宗的还是 gdb 的基本原理: gdb 所认为的目标符号的地址虚拟机中目标符号的实际地址只要这两者能对应上,就没有理由断不到。那如何确认这两者呢?
gdb: info address do_page_fault
guest: cat /proc/kallsyms | grep do_page_fault
比对这两个地址,如果不一样,找出原因来,比如:
没有正确地开启 nokaslr。——很容易手输成 nokalsr 之类的。 vmlinux 与 guest 内核不匹配。……
加入 initrd
有时候我们必须要有用户态环境来测试新编译的内核:比如跑一个 poc 或者测试程序,比如挂载磁盘、做文件系统的操作等等。
这一节,我们引入一个最简单的 initrd,来帮助启动一个基本的用户态操作环境。
init 创建脚本: 大家可以直接使用下面的脚本来创建 initrd:(脚本看起来长,其实内容很少,只是我写的比较罗嗦。)
#!/bin/bash
set -e
INITRD_ROOT=./initrd-rootfs
if [ -e $INITRD_ROOT ];then
rm -rf $INITRD_ROOT
fi
mkdir -p $INITRD_ROOT
cd $INITRD_ROOT
install_simple()
{
mkdir -p etc proc sys lib bin usr
cp /bin/busybox bin/
ln -s busybox bin/cat
ln -s busybox bin/cp
ln -s busybox bin/insmod
ln -s busybox bin/mkdir
ln -s busybox bin/mknod
ln -s busybox bin/mount
ln -s busybox bin/sh
ln -s busybox bin/sleep
ln -s busybox bin/switch_root
ln -s busybox bin/umount
ln -s bin sbin
}
install_auto()
{
mkdir -p etc proc sys lib usr/bin
cp /usr/bin/busybox usr/bin/
busybox --install -s usr/bin/
ln -s bin usr/sbin
ln -s usr/bin bin
ln -s usr/bin sbin
}
install_xxx()
{
# Add any file you want to the INITRD_ROOT
}
# change the comment to select the mod
#install_simple
install_auto
install_xxx
cat << EOF > init
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo
echo "Hello, world!"
exec /bin/sh
EOF
chmod +x init
# Note: must exec cpio in INTIRD_ROOT!
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../myinitrd.cpio.gz
init 脚本有四个基本步骤:
创建一个文件夹 initrd-rootfs,我们就是步骤这个目录里的内容,然后打包为 initrd 的。
在里面布置我们需要的文件,主要是一些基本的命令。——我们基于 busybox 来实现。
需要安装 busybox-static deb 包;另外一个可选项是 klibc,功能没有 busybox 多。我在脚本里展示了两种方式:A 手动添加链接;B 使用 busybox 的 install 选项。建议用 auto,这样会有完整的 busybox 命令。如果大家要自己手写脚本的话,要特别注意软链接之间的关系,很容易犯错。
- 布置/init:
内核发现/init 存在时,会自动执行它,作为用户态的入口程序,pid 为 1。脚本的例子里非常简单,就是 mount proc 和 sysfs,然后执行 bash。可以在 init 里做各种事情,比如加载模块,起 udev 挂磁盘,或者挂载 9p/nfs 之类的作为 root,等等。
- cpio 打包:
建议大家照抄命令。尤其是要注意:进入 INITRD_ROOT 文件夹后跑命令,不会可能文件路径对不上,最后内核找不到/init。
启动:
最后,大家可以用 initrd 来启动:
#!/bin/bash
QEMU=qemu-system-x86_64
KERNEL=4.19-x86/4.19-x86-build/
exec $QEMU \
-enable-kvm \
-m 2048M \
-smp 2 \
-nographic \
-kernel $KERNEL/arch/x86/boot/bzImage \
-initrd myintird.cpio.gz \
-append "console=ttyS0 nokaslr" \
$@ \
exit 0
会看到如下显示:
[ 0.469630] Run /init as init process
Hello, world!
BusyBox v1.36.1 (Debian 1:1.36.1-6) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/bin/sh: can't access tty; job control turned off
~ #
大家可以在自己的环境里测试,也可以直接到 virt-space 里体验,有一个 2-initrd.sh,就是上面的脚本。——实际上已经生成好了 myinitrd.cpio.gz,大家调整一下 1-qemu.sh 里的启动命令,就可以直接跑了,或者直接用:
./1-qemu.sh -initrd myinitrd.cpio.gz
ok,到这里,我们就可以使用基本的用户环境了。
扩展 initrd
busybox:
busybox 提供的命令很多,基本命令都涵盖了:
Currently defined functions:
[, [[, acpid, adjtimex, ar, arch, arp, arping, ascii, ash, awk, base64, basename, bc,
blkdiscard, blockdev, brctl, bunzip2, busybox, bzcat, bzip2, cal, cat, chgrp, chmod,
chown, chroot, chvt, clear, cmp, cp, cpio, crc32, crond, crontab, cttyhack, cut, date,
dc, dd, deallocvt, depmod, devmem, df, diff, dirname, dmesg, dnsdomainname, dos2unix,
dpkg, dpkg-deb, du, dumpkmap, dumpleases, echo, ed, egrep, env, expand, expr, factor,
fallocate, false, fatattr, fdisk, fgrep, find, findfs, fold, free, freeramdisk,
fsfreeze, fstrim, ftpget, ftpput, getopt, getty, grep, groups, gunzip, gzip, halt,
head, hexdump, hostid, hostname, httpd, hwclock, i2cdetect, i2cdump, i2cget, i2cset,
i2ctransfer, id, ifconfig, ifdown, ifup, init, insmod, ionice, ip, ipcalc, kill,
killall, klogd, last, less, link, linux32, linux64, linuxrc, ln, loadfont, loadkmap,
logger, login, logname, logread, losetup, ls, lsmod, lsscsi, lzcat, lzma, lzop,
md5sum, mdev, microcom, mim, mkdir, mkdosfs, mke2fs, mkfifo, mknod, mkpasswd, mkswap,
mktemp, modinfo, modprobe, more, mount, mt, mv, nameif, nbd-client, nc, netstat, nl,
nologin, nproc, nsenter, nslookup, nuke, od, openvt, partprobe, passwd, paste, patch,
pidof, ping, ping6, pivot_root, poweroff, printf, ps, pwd, rdate, readlink, realpath,
reboot, renice, reset, resume, rev, rm, rmdir, rmmod, route, rpm, rpm2cpio, run-init,
run-parts, sed, seq, setkeycodes, setpriv, setsid, sh, sha1sum, sha256sum, sha3sum,
sha512sum, shred, shuf, sleep, sort, ssl_client, start-stop-daemon, stat, strings,
stty, su, sulogin, svc, svok, swapoff, swapon, switch_root, sync, sysctl, syslogd,
tac, tail, tar, taskset, tc, tee, telnet, telnetd, test, tftp, time, timeout, top,
touch, tr, traceroute, traceroute6, true, truncate, ts, tty, tunctl, ubirename,
udhcpc, udhcpc6, udhcpd, uevent, umount, uname, uncompress, unexpand, uniq, unix2dos,
unlink, unlzma, unshare, unxz, unzip, uptime, usleep, uudecode, uuencode, vconfig, vi,
w, watch, watchdog, wc, wget, which, who, whoami, xargs, xxd, xz, xzcat, yes, zcat
但是,有可能对于我们测试内核而言还是不够,比如:
- 测试需要内核模块
- 测试需要挂载磁盘里的文件系统
……
这篇文章里我暂时不准备做详细的说明,但是可以提供一些参考给大家:
- 大家感兴趣的话,可以看一下本机的/init:/usr/share/initramfs-tools/init,看它怎么完成各种 kernfs 的挂载,解析 cmdline,实现 break,挂载 root,最后 switch_root。
- 另外一个值得参考的则是 liveboot 的 init,它会找到 readonly 的 squashfs image,将它挂起来后再用 tmpfs 来做一层 overlay,实现 rw 的根文件系统。
- 之后还会介绍到另外一种裸启动内核 image 的 virtme-ng 项目,也是在 init 里做了很多工作,比如将 host 导出的 9p 目录,挂载为 guest 里的 read-only 根。
对于内核模块:
- 可以简单的复制到 INITRD_ROOT,然后在 initramfs 的 shell 里,手动 insmod。——自己注意一下依赖关系就好。
- 之后会介绍的 virtme-ng 项目能帮我们处理好模块依赖关系,以及复杂的用户态测试环境。——这也是它最核心的部分。
对于磁盘:
- 规范流程是起 systemd-udevd,然后用 udevadm trigger + settle,再手动 mount;
- 也可以简单一点,把相关的 ko(比如对应 fs)打包到 initrd 里,然后手动走 mount 流程。
qemu+disk 启动
如果我们编译的内核需要长时间的调试,或者调试它所需要的用户态环境比较复杂,手动构建的 initrd 不能满足要求,那我们可以考虑用一个稳定的磁盘来启动,安装内核 deb 包并测试,就和物理机器一样。
安装
虚拟机的安装:
- 推荐用 virt-manager 的图形化界面来安装。
- 也可以用 qemu 命令:
qemu-img create -f qcow2 uos.img 256G
qemu-system-x86_64 -enable-kvm -m 2048M -smp 2 -hda uos.img -cdrom uos.iso
注意,如果用 qemu 安装的话,是要图形界面支持的,所以在 ssh 里会启动不了(之前已经提到了,在 ssh 里要用 nographic);如果在本地的话那都没问题。
启动
安装完成后(不管是用 qemu 还是 virt-manager 安装的),就可以直接基于 disk 启动了,比如:
qemu-system-x86_64 -enable-kvm -m 2048M -smp 2 -hda uos.img
需要更新内核时,就编译好内核之后,想办法将 deb 包传到 guest 里并安装。——这样,guest 里自始至终都是一个完整、自洽的系统环境,可以跑 bpftrace、trace-cmd、crash 等各种工具。
hda/vda
hda 会虚拟一个 hard-disk 设备,在虚拟机里会被导出为 sda,其实和“-s”一样,也只是一个语法糖,等同于:
-drive file=uos.img,index=0,media=disk
如果想使用 virtio-blk 的话(导出为 vda),可以:
-drive file=uos.img,if=none,id=myvda \
-device virtio-blk-pci,drive=myvda \
在我们的示例里,简单起见,就直接用-hda 了。
文件系统直通
disk-boot + 9p 这里只介绍 disk 启动时的 9p 直通,因为用 kernel/initrd 启动时,需要手动布置 initrd 里的 9p 相关模块,并且更改 init 脚本,我们暂时不想搞这么复杂(以后应该会补充)。
启动脚本如下:
#!/bin/bash
QEMU=qemu-system-x86_64
KERNEL=4.19-x86/4.19-x86-build/
exec $QEMU \
-enable-kvm \
-m 2048M \
-smp 2 \
-nographic \
-hda disk-imgs/uos-base-hda.img \
-fsdev local,security_model=passthrough,id=fsdev0,path=./hostshare \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \
$@ \
exit 0
qemu 参数说明:
- -fsdev:
- -path:指定了要在 host 里要传递给 guest 的目录,这里是/virt-space/hostshare
- -id:qemu 内部使用,这里在-fsdev 里导出,在-device 被使用
- -security_model:9p 的 credits 管理,本地就用 passthrough 吧。
- -device:
- virtio-9p-pci,完全的写法应该是-driver=virito-9p-pci
- mount_tag:guest 里需要用这个 tag 来完成 mount
guest 里挂载:
挂载命令:
mount -t 9p -o trans=virtio,version=9p2000.L hostshare /hostshare
version 可以不指定,但是用 9p2000.L 效率更更高。
也可以写入 fstab:
hostshrae /hostshare 9p nofail,auto,trans=virtio,version=9p2000.L 0 0
之后就可以直接执行:
sudo mount /hostshare
注意,如果在 host 里是用 uos 账户(id1000)启动内核的,那在 passthrough 模式下,guest 里同样只能用 id1000 来访问,可能 root 会没有访问权限……
kernel+disk 启动
可以基于 disk,同时指定 kernel 启动,比如:
#!/bin/bash
QEMU=qemu-system-x86_64
KERNEL=4.19-x86/4.19-x86-build/
exec $QEMU \
-enable-kvm \
-m 2048M \
-smp 2 \
-nographic \
-kernel $KERNEL/arch/x86/boot/bzImage \
-initrd myinitrd.cpio.gz \
-append "console=ttyS0 nokaslr" \
-hda uos.img \
$@ \
exit 0
但是我们的 initrd 环境里,缺乏必要的 ko,而且 init 脚本也没做必要的初始化工作,所以 disk 不可用。那如何用这种方式启动呢?某些时候我们想临时测试一个 kernel,但是又需要随便挂个盘,可以持久化一些数据。
我们暂时不准备用手动方式的实现,而是引入一个上游的专门用于内核启动测试的项目:virtme-ng。
一些我的疑问
做为刚开始了解 qemu 和文件系统的人,补充一些基本问题的解答是有必要的。
什么是 9p
怎么制做 qemu 的镜像呢?
这个韬哥可能以后会讲,如果不讲的话,自己做一些实践吧。
- 内核当中的 bzimage 是什么?
https://blog.csdn.net/hanxuefan/article/details/7454352 ,就是内核镜像。一般使用压缩后的 bzimage 知道这一点就足够了。
- initrd 和 initramfs 这两个是什么?
为了增强内核启动的灵活性而产生的机制,就是这个。尤其是文件系统之类的模块,先有蛋还是先有鸡呢?为了减少循环引用吧。反正这套流程才是目前的正统流程,是符和这个现实世界发展而形成的一套机制。
- 退出 qemu 的命令行界面
C-a x ,用这个快捷键就能把这个关闭了。
- 什么是 busybox ?
https://zhuanlan.zhihu.com/p/448313296
busybox 为什么这么小?显然它也是经过了压缩和删减的,所以只能说是精心设计的丐版软件,恰巧嵌入式的机器喜欢丐版的软件。
- 上面的 ln -s bin usr/sbin 这个命令一度不理解
其实,如果 ln -s 的第一个参数文件名或目录名不存在的话,那就会找第二个参数相同目录下的这个文件名或者目录名,看着挺绕的,可以写的更清楚一点。
- mount -t
mount -t proc none /proc 是一条命令,用于将 proc 文件系统挂载到指定的目录(/proc)。
在 Linux 系统中,/proc 是一个特殊的虚拟文件系统,提供了对内核运行时状态的访问。它不是从硬盘上的文件系统中读取数据,而是通过内核动态生成的,可以获取有关系统当前状态和进程信息的各种数据。通过挂载/proc 文件系统,可以将这些状态信息暴露给用户空间的应用程序或工具。
mount -t proc none /proc 命令的含义是将 proc 文件系统以"none"的方式挂载到/proc 目录下。“none"表示该挂载点没有来源设备,而是虚拟的。这样,当你通过/proc 目录访问系统信息时,实际上是通过读取/proc 文件系统中的虚拟文件来获取相应的信息。
需要注意的是,这是一个常见的操作,通常在系统启动时自动执行,以便让用户和系统工具能够方便地访问/proc 中的信息。
- 适合做什么?
qemu 的部分肯定是适合看一些公共部分的代码的。方便调式和学习。尤其是文件系统和网络的学习用 qemu 是比较好的。虚拟机和 docker 不一样。各有所长。可能最好的调试方法是 kgdb 的方式。kgdb 的方式通过网络是最好的。但是目前来看在 UOS 上还是不好做,需要一个不知道什么原理打上的一个 patch 。多了这么一个步骤,不感觉有一些难度还愿意更深入的了解一下了。
- cpio 压缩命令
https://liubigbin.github.io/2016/02/28/cpio%E5%91%BD%E4%BB%A4%E8%AF%A6%E8%A7%A3/
-H 或者是 –format ,newc ,是一种 SVR4 兼容的模式。
gzip -9 对应的是最大压缩率,可以用 emacs helm-man 查看中文的文档,可以很清楚的理解每一个参数的作用。然后 C-x 4 0 关闭窗口,emacs 真的是很好用的。
- virtme-ng
这是一个针对内核的测试项目。这些内容都是通过网络社区中的内容学习到的开源知识。要想真的熟练掌握调试的话一定要多看多练。
- 什么是 qemu 的 live boot ?
QEMU(Quick EMUlator)是一款用于模拟 CPU 的开源虚拟化软件。使用 QEMU,用户可以在一个物理机器上运行多个虚拟机,每个虚拟机都可以运行不同的操作系统。
Live boot 是指直接从可移动媒介(如 USB 驱动器或 CD/DVD 光盘)启动计算机,并在不安装到硬盘的情况下运行操作系统的过程。
因此,QEMU live boot 就是使用 QEMU 工具,从可移动媒介中引导操作系统并在虚拟机中运行该操作系统的过程。
使用 QEMU live boot,用户可以在单一计算机上同时运行多个操作系统,而无需将它们安装到本地硬盘。这对于测试、教学或者研究等场景非常有用。
- 什么是云镜像?
https://cloudinit.readthedocs.io/en/latest/tutorial/qemu.html
这个云镜像适合快速构建虚拟机,但是没有找到 UOS 版本的,那就暂时不找了。
https://zhuanlan.zhihu.com/p/453495129
可以用云镜像来做 qemu 虚拟机,但是这个对于调式而言并不方便。没有学习的必要。这种东西,是搞 kvm 云服务器的人才需要关注的。
- 调试模块和显卡驱动等要后续继续看
用 qemu 看启动部分的代码是完完全全够用的了,
gdb 高阶用法
C-x 2 打开调试窗口,看起来还挺炫酷的。关闭这个 tui 窗口用的是 C-x a ;和 qemu 的退出快捷键不一样。qemu 的退出用的是 C-a x 很相似。
如果是一个公共的问题,mm 或者是 fs 的问题,学习用的话,那么 qemu 真的是不二之选。
info variables
info locals
info args
这样做的话就能看到足够多的变量了,发现内核当中的变量都被优化掉了,那么也就不能进行调式了。
内核 gdb 被优化掉了的解决方法。
解决方法下面的链接有,以后有时间研究一下,或者问问其他人看是不是有人清楚是怎么回事呢?
内核 gcc 编译用 Og
https://www.cnblogs.com/zhchy89/p/8805691.html
前几天我发现了一个上游未合入的 patch ,与韬哥讨论,韬哥用 b4 下载下来了 patch ,并在我们的内核上进行了合入。 https://blog.51cto.com/u_15127700/3606710
kernel hacking: GCC optimization for debug experience (-Og) [LWN.net]
patch 是 2018 年 intel 的一位国人工程师提交的,大概就是添加了两个构建编译过程中的内核配置选项,来让调试的时候更舒服一点:
- 关闭 auto inline
- 使用-Og,而不是默认的-O2
同时因为某些内核代码可能会在新的配置下有编译 warning,所以作者还手动做了一些小 fix,都不涉及程序语义改动。
这个 patch 发了几个版本,上游基本没什么反对意见;但貌似因为还有一些编译 warning 没有得到修复(根据 kbuild 维护者反馈),所以最终不了了之了。
韬哥拉取了上游最新的 patchset,简单修复了一些合并冲突(无语义改动),推到了我们的 4.19 x86 分支: kernel hacking: new config CC_OPTIMIZE_FOR_DEBUGGING to apply GCC -Og optimization (I8ce1d36b) · Gerrit Code Review
韬哥简单测试了一下,默认编译配置下,kallsyms 大概是 98000 个符号;关闭自动 inline 之后,会增加到 107000 左右。至于-Og 和-O2 的区别,我没有实际研究……
这两个配置默认都是关闭的,如果大家发现调试 trace 时有某些 static 函数被 inline 了,那可以考虑打开这个配置再编译调试内核。
至于-Og……从上游的描述来看应该是接近于-O1,但我也不知道实际调试会怎么需要它。
vng 启动虚拟机
文章 udoc 地址: https://udoc.uniontech.com/s/kZK93TyLFMyECgZ
虚拟化专题地址: https://udoc.uniontech.com/s/JT6W9XWFgrK9CTa
完整的 kernel-base 启动
在之前的文章里,我们展示了如何用 qemu+kernel 做最简单的启动,它会因为找不到 initrd 和 rootdev 而 panic;我们也可以手动用 busybox,加上我们手写的/init 脚本来构建一个最简单的 initrd,在最后调用/bin/sh 来进入一个可用的用户态命令行环境;当然我们也可以直接基于 qemu+disk 启动,在完整的持久化系统环境里去更新内核,做测试。但是我们在开发/修改内核时,时常会有这样的需求:
- 修改、编译一个内核以后,可以一键快速完成启动,最好是秒级内完成。
- 新内核可以直接启动,不需要安装、更新软件包。
- 不需要修改内核配置,最好是可以在之前的构建目录里直接使用,无侵入性。
- 内核启动后,可以在需要时加载正确的 ko 模块。(本次编译的 ko)
- 虚拟机启动后,和宿主机之间有文件系统直通。
- 虚拟机启动后,有完整的开发调试环境,比如各种调试工具,如 strace、bpf 等。
- ……
这样的需求是可能实现的吗?有相关的路径存在吗?
答案肯定的:
- qemu 可以基于 kernel+initrd+cmdline 启动。
- 我们可以定制一个合适的 initrd,包含挂载 9pfs 的相关 ko(比如 9pfs,9pnet,virtio 等),同时写一个/init 脚
本,将 host 的“/”直接挂载为 guest 的“/”(只读)。——这样,宿主机的环境就完整地传递到了虚拟机里。
- 我们可以基于编译目录,手动构建一个/lib/modules/0.0.0/目录,模拟出系统的模块目录(可以被 modprobe 识别)
,然后想办法 mount 到 guest 的“uname -r”对应目录去。——这样,guest 就能找到正确版本的 ko。
- 我们可以另外指定一些目录,用 rw 的方式,通过 9pfs 直通给 guest。
- 从上面的分析可以看出来,核心的麻烦点其实是 initrd 和内核模块:
- 对于内核模块,我们不但要把挂载 rootfs 相关的模块部署到 initrd 里,还要构建一个模块目录来给 guest 里
的 modprobe 使用。
- 对于 initrd,除了 ko 之外,还需要定制一些控制逻辑,来完成 9p 文件系统的挂载,以及其他的各种环境相关
的配置。——比如在 read-only 的 9p root 之上,可能还要用 tmpfs+overlayfs 来把一些目录(比如 etc 之类的)挂载为 rw 的。
既然这么好,那我们之前的基于 qemu+kernel+initrd 的启动方案,为什么没有实现上面这些要求呢?因为已经存在一个上游项目,帮助我们完整地实现了上面的需求,也就是 virtme-ng 项目。
virtme-ng 启动
virt-space 体验
# 进入编译目录
ssh [email protected]
cd /virt-space
cd 4.19-x86/4.19-x86-build/
# 一键启动,vng是主命令
sudo /virt-space/virtme-vng/vng
启动成功的输出如下:
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ sudo /virt-space/virtme-ng/vng
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 4.19.0-amd64-desktop x86_64
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# uname -a
Linux virtme-ng 4.19.0-amd64-desktop #1 SMP Fri Jan 5 11:53:01 CST 2024 x86_64 GNU/Linux
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# exit
exit
可以看到,在虚拟机里,vng 会自动修改 hostname 为 virtme-ng,同时在内核名称加上“virtme-ng”的前缀作为区分。
ok,就是这么简单,vng 启动完成!
注意,在 uos-4.19 上运行时,请直接用 root 启动,可以避免一些很繁琐的问题。
在自己的环境里部署运行 vng
如果大家想在自己的机器上跑 vng 的话,也很简单,因为 vng 项目是基于 python 开发的,本身就是 python 脚本,可以直接运行。——上面的/virt-space/virtme-ng/实际上就是源码目录。
所以大家可以直接拷贝 x86-nodepc 里 virtme-ng,也可以到 github clone https://github.com/arighi/virtme-ng
基于源码路径运行时,需要预先安装一些软件包:
sudo apt install python3-pip python3-argcomplete flake8 pylint qemu-system-x86
然后就可以基于源码目录启动 vng 了。
vng 环境
vng 启动的虚拟机里,有完整的程序环境,比如——我们可以随便跑一个 bpftrace 程序:(host 里已经安装了 bpftrace)
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# bpftrace -e 'profile:hz:1 / cpu == 0 /{ printf("cpu %d comm %s:%s\n", cpu, comm, kstack);} interval:s:3 { exit(); }'
Attaching 2 probes...
cpu 0 comm swapper/0:
native_safe_halt+14
__cpuidle_text_start+28
do_idle+467
cpu_startup_entry+111
start_kernel+1287
secondary_startup_64+164
cpu 0 comm swapper/0:
native_safe_halt+14
__cpuidle_text_start+28
do_idle+467
cpu_startup_entry+111
start_kernel+1287
secondary_startup_64+164
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
或者跑一个 strace 程序:
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# strace -e %%stat ls > /dev/null
fstat(3, {st_mode=S_IFREG|0644, st_size=119161, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=163488, ...}) = 0
fstat(3, {st_mode=S_IFREG|0755, st_size=1779440, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=468944, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=14592, ...}) = 0
fstat(3, {st_mode=S_IFREG|0755, st_size=146968, ...}) = 0
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=6174592, ...}) = 0
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x3), ...}) = 0
+++ exited with 0 +++
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
host 里的几乎所有的命令(甚至包括嵌套 vng)都可以直接在 guest 里直接运行!(这就很神奇,为啥呢)
文件系统与直通
vng 里的文件系统
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# findmnt
TARGET SOURCE FSTYPE OPTIONS
/ /dev/root 9p ro,relatime,sync,dirsync,access=any,
├─/proc proc proc rw,nosuid,nodev,noexec,relatime
├─/sys sys sysfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/debug debugfs debugfs rw,relatime
│ ├─/sys/kernel/tracing tracefs tracefs rw,relatime
│ ├─/sys/kernel/security securityfs security rw,relatime
│ └─/sys/fs/cgroup cgroup2 cgroup2 rw,relatime
├─/tmp tmpfs tmpfs rw,relatime
├─/run run tmpfs rw,relatime
├─/etc virtme_rw_overlay0 overlay rw,relatime,lowerdir=/etc,upperdir=/
│ ├─/etc/fstab tmpfs[/fstab] tmpfs rw,relatime
│ ├─/etc/hosts tmpfs[/hosts] tmpfs rw,relatime
│ ├─/etc/shadow tmpfs[/shadow] tmpfs rw,relatime
│ └─/etc/sudoers tmpfs[/tmp.tn6fSp1251]
│ tmpfs rw,relatime
├─/usr/lib virtme_rw_overlay1 overlay rw,relatime,lowerdir=/lib,upperdir=/
├─/home virtme_rw_overlay2 overlay rw,relatime,lowerdir=/home,upperdir=
├─/opt virtme_rw_overlay3 overlay rw,relatime,lowerdir=/opt,upperdir=/
├─/srv virtme_rw_overlay4 overlay rw,relatime,lowerdir=/srv,upperdir=/
├─/usr virtme_rw_overlay5 overlay rw,relatime,lowerdir=/usr,upperdir=/
│ ├─/usr/lib/modules none tmpfs rw,relatime
│ ├─/usr/lib/dpkg-db/lock tmpfs[/lock] tmpfs rw,relatime
│ ├─/usr/lib/dpkg-db/lock-frontend
│ │ tmpfs[/lock-frontend]
│ │ tmpfs rw,relatime
│ └─/usr/lib/dpkg-db/triggers/Lock
│ tmpfs[/Lock] tmpfs rw,relatime
├─/var virtme_rw_overlay6 overlay rw,relatime,lowerdir=/var,upperdir=/
│ ├─/var/tmp tmpfs tmpfs rw,relatime
│ ├─/var/spool/rsyslog tmpfs tmpfs rw,relatime
│ ├─/var/log tmpfs tmpfs rw,relatime
│ ├─/var/cache tmpfs tmpfs rw,relatime
│ ├─/var/lib/apt tmpfs tmpfs rw,relatime
│ └─/var/lib/private tmpfs tmpfs rw,relatime
└─/dev devtmpfs devtmpfs rw,nosuid,noexec,relatime,size=49092
├─/dev/pts devpts devpts rw,nosuid,noexec,relatime,gid=5,mode
└─/dev/shm tmpfs tmpfs rw,nosuid,nodev,relatime
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# exit
exit
可以看到,类似于 host,vng 的 guest 里也挂载了各种系统必须的 kernfs 和 tmpfs,比如 proc,sysfs,debugfs,tracefs, cgroupfs,devtmpfs,/tmp,/run,/dev/shm 等等。
另外,默认 vng 是将 root 挂载为 read-only,同时将 usr,lib,etc,home,opt,srv 等目录都用 tmpfs 做了 rw 的 overlay,是可以写入的。——甚至可以在 guest 里安装新的 deb 包,只是要注意给 guest 的 memory 设置得大一点。(默认是 1G,装大软件是不太够的)
另外我们也可以通过参数“–rwdir PATH”,直通另外的目录给 guest,并设置为可读可写:
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ sudo /virt-space/virtme-ng/vng --rwdir /virt-space/hostshare/
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 4.19.0-amd64-desktop x86_64
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# touch /virt-space/hostshare/hello-vvvng
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# ls -l /virt-space/hostshare/hello-vvvng
-rw-r--r-- 1 root root 0 1月 16 2024 /virt-space/hostshare/hello-vvvng
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# exit
exit
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$
快速启动与常用配置
快速启动
我们可以指定 vng 在 guest 里执行某个特定的命令,并在执行完成后自动退出,下面的例子里我们可以用“uname -a”来测试虚拟机启动的整体耗时:
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ time sudo /virt-space/virtme-ng/vng -- uname -a
Linux virtme-ng 4.19.0-amd64-desktop #1 SMP Fri Jan 5 11:53:01 CST 2024 x86_64 GNU/Linux
real 0m5.925s
user 0m9.704s
sys 0m12.036s
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$
注意,如果是在内核编译目录第一次执行,因为要解析和构建内核模块相关的临时目录,可能会要慢一些。
我一般是不调整任何内核配置、构建流程,在源码目录里编译以后(不管是 make,还是 make bindeb-pkg),直接到该目录里启动 vng。甚至一般来说都可以不用调整配置,基本配置可以满足大部分的测试需求了。
一些小的 tips:
- 如果发现某个软件没有,我一般会直接在 host 里安装,guest 里会直接有,当然一般建议重启 guest,反正很快。
- 如果有一些需要在 guest 里反复进行的小操作,那就直接在编译目录里写一个小脚本吧。——guest 启动
后,会自动 chdir 到我们启动 vng 的目录,使用起来非常顺手。
除了上面的“–rwdir PATH”之外,还有一些基本配置是我们可能比较需要的:
添加磁盘 “–disk xxx.img”
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ dd if=/dev/zero of=test.img bs=1M count=300
记录了300+0 的读入
记录了300+0 的写出
314572800 bytes (315 MB, 300 MiB) copied, 0.0868311 s, 3.6 GB/s
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ sudo /virt-space/virtme-ng/vng --disk test.img
请输入密码:
验证成功
WARNING: Image format was not specified for 'test.img' and probing guessed raw.
Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
Specify the 'raw' format explicitly to remove the restrictions.
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 4.19.0-amd64-desktop x86_64
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
fd0 2:0 1 4K 0 disk
sr0 11:0 1 1024M 0 rom
vda 254:0 0 300M 0 disk
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# exit
exit
设置 cpu、memory “–cpus N –memory XX”
默认下,cpu 数量是和 host 一样,memory 是 1G,我们可以手动调整:
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ sudo /virt-space/virtme-ng/vng --cpus 4 --memory 2G
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 4.19.0-amd64-desktop x86_64
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# nproc
4
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# free -h
total used free shared buff/cache available
Mem: 1.9Gi 28Mi 1.9Gi 0.0Ki 24Mi 1.8Gi
Swap: 0B 0B 0B
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build# exit
exit
透传 qemu-option,内核 cmdline “-o"xxx” –append “xxx””
比如想用 gdb 调试 vng 启动的 guest,就需要用到这两个选项,下一节里我们可以看到详细的例子。
值得注意的是,使用-o 选项时,后面不要带空格,也不要用”–qemu-opts“这个格式。——参数解析时有一些小 bug,等我有空时修了它……
总之,大家就先用-o。
其他的选项大家可以用 vng –help 查看,network、graphic、sound 这些我也暂时没有用过。
再用 gdb 调试
启动 guest
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ sudo /virt-space/virtme-ng/vng -o"-gdb tcp::7761" --append "nokaslr"
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 4.19.0-amd64-desktop x86_64
root@virtme-ng:/virt-space/4.19-x86/4.19-x86-build#
qemu opts 里,可以用“-s”快捷表示 tcp::1234,可以加-S 让虚拟机停住等 gdb。
启动 gdb
uos@uos-x86-nodepc:/virt-space/4.19-x86/4.19-x86-build$ gdb vmlinux
GNU gdb (Uos 8.2.1.1-1+security) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from vmlinux...done.
(gdb) target remote:7761
Remote debugging using :7761
0xffffffff817f39ce in native_safe_halt () at ./arch/x86/include/asm/irqflags.h:60
60 asm volatile("sti; hlt": : :"memory");
(gdb) bt
#0 0xffffffff817f39ce in native_safe_halt () at ./arch/x86/include/asm/irqflags.h:60
#1 0xffffffff817f361c in arch_safe_halt () at ./arch/x86/include/asm/paravirt.h:94
#2 default_idle () at arch/x86/kernel/process.c:557
#3 0xffffffff810b7703 in cpuidle_idle_call () at kernel/sched/idle.c:153
#4 do_idle () at kernel/sched/idle.c:263
#5 0xffffffff810b795f in cpu_startup_entry (state=CPUHP_ONLINE) at kernel/sched/idle.c:369
#6 0xffffffff817e6333 in rest_init () at init/main.c:441
#7 0xffffffff826c4094 in start_kernel () at init/main.c:736
#8 0xffffffff810000d4 in secondary_startup_64 () at arch/x86/kernel/head_64.S:243
#9 0x0000000000000000 in ?? ()
(gdb) b do_page_fault
Breakpoint 1 at 0xffffffff8106afc0: file ./arch/x86/include/asm/paravirt.h, line 57.
(gdb) c
Continuing.
[Switching to Thread 16]
Thread 16 hit Breakpoint 1, do_page_fault (regs=0xffffc900007b7f58, error_code=20)
at ./arch/x86/include/asm/paravirt.h:57
57 return PVOP_CALL0(unsigned long, pv_mmu_ops.read_cr2);
(gdb) bt
#0 do_page_fault (regs=0xffffc900007b7f58, error_code=20)
at ./arch/x86/include/asm/paravirt.h:57
#1 0xffffffff8180117e in async_page_fault () at arch/x86/entry/entry_64.S:1208
#2 0x0000000000000001 in irq_stack_union ()
#3 0x0000000000000000 in ?? ()
(gdb)
可以看到,gdb 调试一切如常。
已知问题
.virtme_mods 残留问题
如果布置这个文件夹到一半的时候被中断了,可能会导致 vng 内部逻辑混乱(这也算是一个带修复的 bug),导致各种报错。
大家可以手动执行这个脚本修复:src/virtme/scripts/virtme-prep-kdir-mods
uos4.19 普通用户权限问题
可以启动内核,但是在 init 脚本里执行 overlay 时会有一些问题。
没细查,大家直接用 root 启动吧。
总结
对于 vng 的介绍就大概到这里,主要还是介绍它的实际使用为主。
如果有同事对它的内部实现感兴趣的话,也可以直接阅读其源代码,核心文件有:
- src/virtme/virtmods.py : 内核 ko list
- src/bin/virtme-prep-kdir-mods : 布置.virtme_mods 内核模块目录
- src/virtme/mkinitramfs.py : 构建 initrd,包括/init
- src/virtme/guest/virtme-init : guest 里的 1 号进程
- src/virtme/commands/run.py : virtme 的主流程
- src/virtme_ng/run.py : virtme-ng 的主流程
virtme 和 virtme-ng:
- virtme 是很早就有的上游项目,实现了核心功能。
- ubuntu 的内核团队发现了该项目,想用起来,但是发现已无人维护;所以 fork 成了 virtme-ng,同时基于 virtme 封
装了一个 virtme-ng 以及 vng 命令,自动完成很多的配置,使得可以方便的一键快速启动。
所以,当我们使用“vng”时,实际上 virtme-ng 会帮我们传递很多的参数给 virtme,最终通过 qemu 启动虚拟机。而整个 virtme-ng 项目,实际上就是一个超大的 qemu 启动脚本,尤其是 initrd 和内核模块相关的处理。
另外,virtme-ng 项目里实际上还包含了基于 host 上的已安装内核启动虚拟机、在内核源码目录里自动配置和编译等功能,但我用得很少,我自己更多的还是基于我前置已经完成构建的编译目录去启动 kernel image。
大家如果在使用 vng 的过程里遇到了任何问题,或者觉得有地方不顺手希望可以增加新的特性,都可以跟我联系。一方面我可以做一些简单的 fix,或者尝试着根据我们的使用场景做一些扩展开发;另一方面如果我自己搞不定的话,也可以尝试着向上游开发者去反馈。
ok,vng 篇就到这里,后续我们会详细介绍一下内核的 gdb 调试。
看了文档自己再搭建一下环境
todo ,还是选把 mesa 看看。