计算机 · 2021年9月17日 0

Debug Kernel With QEMU and GDB

想到自己搞了这么久内核居然还不会单步调试,心里感到特别惭愧和无能。做个笔记记录一下如何使用QEMU和GDB来单步调试内核。本来想先研究下用VirtualBox加串口来调试的,奈何发现自己的键盘没有SysRq键,不想重新映射键盘,也有点担心最后的效果可能真的没有QEMU效果好,所以暂时先弄QEMU好了。为什么VirtualBox调试的时候需要SysRq键呢,因为我用宿主机的GDB通过串口连VirtualBox里的虚拟机时,居然无法通过GDB的Ctrl+C中断正在执行的虚拟机里的Linux,只能在虚拟机里通过往/proc/sysrq-trigger里写g命令来暂停操作系统的运行,可是这么做根本没有意义,因为我用GDB单步调试内核的目标场景就是系统出现故障无法响应的时候,通过GDB看下当前堆栈信息,找出是哪里出了错,这个目标场景下根本就没有shell可用。这个g命令本来是可用Alt+SysRq+g组合键实现的,可是我的键盘把SysRq键映射成了PrintScreen键,于是这个组合键就暂时用不了,需要自己手动去修改键盘映射。内核文档里有写SysRq键被映射为PrintScreen键之后怎么改回来的说明,可是我暂时没弄懂这个改键原理,就先暂时搁置算了。

概述

想要达成使用QEMU调试内核代码的目标,需要做到以下几点:

  • 准备rootfs
    只有一个内核的话没啥可调的(至少对本菜鸟是这样),所以我们希望QEMU运行的最好是一个日常使用的Linux发行版一样的东西,有帐号管理,有各种命令行工具,我们在这个系统里面重现出Bug的场景,然后通过宿主机的GDB连上这个系统的内核,开始诊断问题。这个准备rootfs的工作就相当于定制我们自己的发行版了:
  • 制作一个虚拟硬盘;
  • 把我们想要的软件都装进去;
  • 内核的话,看情况我们有三种选择:使用各种官方发行版里面自带的内核;安装自己编译或者从其他地方搞过来的内核到虚拟硬盘里;不安装内核,在QEMU的启动选项里指定内核文件的位置; 如果有精力的话,可以去看Linux From Scratch这本书,对制作Linux发行版将会有更深刻和全面的理解。
  • 编译kernel
  • 启动QEMU
  • 使用GDB连接正在运行的内核

动手实验

只有通过动手实验才能记忆的更深刻,才有机会发现各种意想不到的问题。下面记录一下我的实验过程,基本是按照后面列的别人的博客来走的,加上一点点自己的疑问和思考。

准备rootfs

1.准备一个虚拟硬盘

   qemu-img create debian-stretch-image.img 4g
   mkfs.ext2 debian-stretch-image.img

根据自己预估的要往rootfs里装的软件大小,可以将硬盘的大小设为适当的容量;文件系统也可以用其他的,比如ext4。

2.挂载虚拟硬盘

   # 创建挂载点
   mkdir mp
   sudo mount -o loop debian-stretch-image.img mp

loop选项的说明看mount的manual吧。

3.往虚拟硬盘里装必要的东西

装一个基本的Debian系统

sudo debootstrap --arch amd64 jessie mp 

如果选的是jessie的话,那么后面启动的时候是直接root帐号登录的,如果选的是stretch的话,那么需要先在rootfs里创建用户帐号和设置root密码才行。
创建账户和设置root密码:

sudo chroot mp 
passwd root 
# 设置root密码 
useradd -s '/bin/bash' -m -G adm,sudo myusername 
passwd myusername 
# 设置myusername账户密码 
exit 

在执行debootstrap一步后,我们的虚拟硬盘里面就已经有一个比较完整的系统debian系统了,我们甚至可以在里面使用apt命令装软件。方法就是在chroot之前把dns解析配置拷贝一份进去,然后在chroot进去执行apt命令安装:

sudo cp -b /etc/resolv.conf temp/etc/resolv.conf 
sudo chroot mp 
apt update 
# 爱装啥装啥 
exit 

你要是想在里面装一个内核而不是使用自己另外编译好的内核的话,那么就在chroot之后安装一下linux-image-xxx之类的包就可以了。

装一个Ubuntu的系统

安装Ubuntu Base的包:

wget http://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/ubuntu-base-16.04-core-amd64.tar.gz
tar xf ubuntu-base-16.04-core-amd64.tar.gz -C mp/ 


剩下的步骤和装debian的就完全一样了。

直接装一个完整的发行版

一个疑问


我看这篇博客在chroot之前重新挂载了/sys/proc/dev等目录,可是像这篇博客又没有做这个操作,我自己实验的时候也没有这么做,并不清楚这个步骤的必要性。

mount --rbind /sys /mnt/sys 
mount --rbind /proc /mnt/proc 
mount --rbind /dev /mnt/dev 
chroot <your-mount-point>

4.卸载虚拟硬盘

sudo umount mp

编译kernel

我不是不会编译内核,可是人家的总结确实很简练:

git clone --depth=1 git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
make x86_64_defconfig
make kvmconfig
make -j 8

需要注意的是,我们为了顺利达成GDB的调试目的,那么在make x86_64_defconfig这步之后,去改下这个生成的.config文件,里面添加或者修改一句CONFIG_DEBUG_INFO=y,再接着执行后面的编译步骤。
编译完成之后,给QEMU运行的是bzImage文件,给GDB调试的是vmlinux文件。

启动QEMU

终于可以运行自己的kernel了:

sudo qemu-system-x86_64 -kernel <path-to-bzImage> -hda debian-stretch-image.img -append "root=/dev/sda"
  • QEMU选项里加上-s选项,以提供串口通信,这样才能用GDB调试这个正在运行的kernel;
    嫌用串口麻烦的话,直接连QEMU监听的1234的TCP端口就可以了,只要开启了-s选项,QEMU就默认提供TCP通信;
  • append选项后面跟的字符串是提供给内核作为启动参数的;为了防止内核KASLR给GDB带来的调试困难,在内核的启动选项里加上nokaslr选项;
  • 为了避免QEMU对虚拟硬盘格式的warning,把-hda debian-stretch-image.img换成-drive file=debian-stretch-image.img,index=0,media=disk,format=raw

所以,最终版本的QEMU运行命令是:

sudo qemu-system-x86_64 -kernel linux/arch/x86_64/boot/bzImage -drive file=debian-stretch-image.img,index=0,media=disk,format=raw -append "root=/dev/sda nokaslr" -s

其实还有很多可以调的选项,比如开启kvm的--enable-kvm,但这些算是QEMU的进阶知识,下次再总结吧。

使用GDB连接正在运行的内核

gdb vmlinux
target remote localhost:1234

然后就可以像调试应用程序一样调试这个内核了。为了GDB调试的时候用的爽,可以考虑像linux-kernel-labs一样,下载个GDB的配置文件:wget -P ~ git.io/.gdbinit

资料

旧版本Ubuntu镜像下载地址

这个链接还真是不太好找,就像官方不想让你知道只想让你使用最新版一样。