内核调试黑魔法:对QEMU自身进行调试,从而定位DragonOS问题

前言

与CPU、硬件打交道的时候,有时候看不出自己的代码或者系统出现了什么问题,这时候内核调试工具就显得尤为重要了。

在之前,我们会使用gdb连接到qemu,来获取DragonOS虚拟机里面的一些数据。但是,当涉及到驱动程序、中断及内存管理,我们有时候实在看不出自己的问题在哪里。这个时候我们想,如果qemu虚拟机能够把它模拟的设备的状态输出出来,让我们获得更多的信息,那就太好了。

在本文中,我将讲解调试QEMU自身的思路。

思路

调试QEMU的方式主要有2种:

  • 加日志打印:qemu_printf()
  • 使用gdb调试QEMU自身

整个调试的过程,主要就是打印日志,以及使用gdb去打印寄存器/局部变量值,还有traceback,还有watch指定的内存地址,观察数据与预期是否一致,观察数据被修改的时间点,找到异常点。然后再根据代码,去分析是如何产生这个错误的。接着再返回来看DragonOS里面的硬件相关代码,判断它到底是哪里写错了。

编译安装qemu

在调试之前,我们需要先编译安装QEMU。网上的教程很多,这里我就讲一下大概的思路:

  • 下载QEMU指定版本的代码的压缩包,然后把解压后的目录初始化为git仓库,并commit一次。以便记录我们后面为了调试而加的代码
  • 编译QEMU的时候需要注意:

在build目录下进行编译,并且安装到install目录下。不能直接make install,因为这样会覆盖系统原本的qemu。命令如下:

mkdir build
cd build
# 创建编译配置
../configure --enable-slirp
# 编译
make -j $(nproc)
# 安装到当前目录下的install_dir文件夹
DESTDIR=$(pwd)/install_dir make install

然后我们在启动DragonOS的时候,修改run-qemu.sh里面的这个地方,改为使用你编译的qemu进行启动:(也就是在前面加上一个路径前缀,指向你安装目录下的那个usr/local/bin/目录

image

调试过程中,每次更改qemu的代码后,都重复上述编译命令和安装命令就行。(注意,如果修改了.h文件,则要先make clean后再编译,否则可能造成奇怪的行为与预期不一致的问题。)

如何定位到代码?

上述两种思路,都需要定位到QEMU内的代码。阅读代码这块我就不多说了,使用vscode+clangd就能很方便的跳转(自带的那个intellisense效果不好)。

  • 我们需要先上网搜索,了解一下qemu的代码结构,知道大概是去哪几个目录找代码。
  • 可以先通过qemu的trace功能,查看所有的日志追踪点,启用你觉得相关的追踪点。然后看qemu的日志。根据日志输出的格式,去搜可能符合的格式字符串(因为tracepoint的字符串就是类似printf的那个格式),这样就基本确定“qemu必定经过的代码路径“了。
  • 接着我们可以改qemu的代码使用qemu_printf()函数去不断的打印日志,同时在纸上画调用链。找到整个调用链。(当然这里开始就能使用gdb去辅助调试了)
  • 我们还可以使用gdb打断点+单步执行+traceback的方式去定位代码。

如何使用gdb调试QEMU自身?

网上很多教程都是教我们如何去调试QEMU里面的guest OS的,但是我们如果想获取qemu的中间状态,那么我们得让GDB去调试QEMU自身。

这一步需要编写一个调试脚本,我把它命名为command.gdb。整体流程如下:

  • gdb通过该脚本启动
  • GDB加载qemu-system-xxxx作为要调试的文件
  • GDB设置断点
  • 在调试脚本中,运行qemu的命令,启动虚拟机。

很重要的一点就是获取到启动DragonOS的虚拟机的命令,这个我们可以通过修改DragonOS仓库的tools/run-qemu.sh来获得:

https://code.dragonos.org.cn/xref/DragonOS/tools/run-qemu.sh?r=01090de77ef263b81edf449b77320d5fa28569de#185

我们只需要在181或185行前面,加一行来输出下面要执行的命令,就能知道命令的细节了。

比如,对于riscv的而言,我电脑的输出值是:(这个对于不同版本的dragonos都不一样,请以实际为准)

-kernel arch/riscv64/u-boot-v2023.10-riscv64/u-boot.bin --nographic -d ../bin/disk-riscv64.img -m 4G -smp 2,cores=2,threads=1,sockets=1 -boot order=d -d cpu_reset,guest_errors,trace:virtio*,trace:e1000e_rx*,trace:e1000e_tx*,trace:e1000e_irq* -s -machine virt,memory-backend=dragonos-qemu-shm.ram -cpu sifive-u54 -drive id=disk,file=../bin/disk-riscv64.img,if=none -device ahci,id=ahci -device ide-hd,drive=disk,bus=ahci.0 -netdev user,id=hostnet0,hostfwd=tcp::12580-:12580 -device virtio-net-pci,vectors=5,netdev=hostnet0,id=net0 -usb -device qemu-xhci,id=xhci,p2=8,p3=4 -object memory-backend-file,size=4G,id=dragonos-qemu-shm.ram,mem-path=/dev/shm/dragonos-qemu-shm.ram,share=on

接着我们就可以在tools目录下创建一个叫做command.gdb的文件,里面写成这样:

set breakpoint pending on
# 设置成QEMU的路径
file /home/longjin/code/qemu-8.1.4/build/install/usr/local/bin/qemu-system-riscv64

handle SIGUSR2 noprint nostop
handle SIGUSR1 noprint nostop

# 仅供参考,请添加任何你需要的断点。
b accel/tcg/cpu-exec.c:996
b accel/tcg/cpu-exec.c:1047



run -kernel arch/riscv64/u-boot-v2023.10-riscv64/u-boot.bin --nographic -d ../bin/disk-riscv64.img -m 4G -smp 2,cores=2,threads=1,sockets=1 -boot order=d -d cpu_reset,guest_errors,trace:virtio*,trace:e1000e_rx*,trace:e1000e_tx*,trace:e1000e_irq* -s -machine virt,memory-backend=dragonos-qemu-shm.ram -cpu sifive-u54 -drive id=disk,file=../bin/disk-riscv64.img,if=none -device ahci,id=ahci -device ide-hd,drive=disk,bus=ahci.0 -netdev user,id=hostnet0,hostfwd=tcp::12580-:12580 -device virtio-net-pci,vectors=5,netdev=hostnet0,id=net0 -usb -device qemu-xhci,id=xhci,p2=8,p3=4 -object memory-backend-file,size=4G,id=dragonos-qemu-shm.ram,mem-path=/dev/shm/dragonos-qemu-shm.ram,share=on

接着可以使用命令:

gdb -x command.gdb

就能设置断点并启动虚拟机,这个时候,gdb调试的就是QEMU自身了。

关于GDB的调试命令,有详细的介绍,可以上网慢慢学。可以参考:如何使用GDB调试内核 — DragonOS dev 文档

常用的方法

起始断点与普通断点

调试的时候,我们可以一次性设置很多断点。断点按照用途,我个人认为可以分为两类(起始定位断点和普通断点)。起始定位断点是“要调试的上下文必须触发的第一个断点”。gdb启动后,我们先手动关闭所有普通断点,只留下起始定位断点。在触发这个断点时,人工判断满足条件(是我们要调试的上下文),就开启所有的普通断点。这样的话就能快速准确的定位到要调试的上下文。(不然的话断点太多了我们都不好判断)。

监视内存地址

我们可以使用gdb的watch功能,监视QEMU的结构体的成员变量的变化,当变量发生变化时,就可以准确定位“更新状态”的代码上下文。这有助于我们对整个执行流程进行分析。

结语

通过调试QEMU自身来定位DragonOS的内核问题,可以为我们从虚拟机内部提供数据,辅助分析。但是这种方法要求我们去阅读QEMU源码,对QEMU的整体架构要有一定的了解。所以一回生二回熟,可能第一次尝试这种黑魔法的时候,要花很长时间。但是随着不断的练习,慢慢会发现这个方法还是非常的好用的。

1 个赞