利用QEMU+GDB搭建Linux内核调试环境

吾生也有涯,而知也无涯。这篇文章主要讲述利用QEMU+GDB搭建Linux内核调试环境相关的知识,希望能为你提供帮助。


前言对用户态进程,利用gdb调试代码是很方便的手段。而对于内核态的问题,可以利用crash等工具基于coredump文件进行调试。
其实我们也可以利用一些手段对Linux内核代码进行gdb调试,qemu就是一种。
qemu是一款完全软件模拟(Binary translation)的虚拟化软件,在虚拟化的实现中性能相对较差。但利用它在测试环境中gdb调试Linux内核代码,是熟悉Linux内核代码的一个好方法。
本文实验环境:

  • ubuntu 20.04
  • busybox-1.32.1
  • Linux kernel 4.9.3
  • QEMU
  • GDB 10.1
编译内核源码
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
tar -xvzf linux-4.9.301.tar.gz
cd linux-4.9.301
make menuconfig

在内核编译选项中,开启如下"Compile the kernel with debug info"
Kernel hacking --->
Compile-time checks and compiler options --->
[ ] Compile the kernel with debug info

示意图如下,利用键盘选中debug选项,然后敲"Y"勾选:


以上配置完成后会在当前目录生成 ??.config??? 文件,我们可以使用 ??grep?? 进行验证:
grep CONFIG_DEBUG_INFO .config
CONFIG_DEBUG_INFO=y

编译内核
make bzImage -j4

编译完成后,会在当前目录下生成vmlinux,这个在 gdb 的时候需要加载,用于读取 symbol 符号信息,包含了所有调试信息,所以比较大。
压缩后的镜像文件为bzImage, 在arch/x86/boot/目录下。
?linux-4.9.301 ls -hl vmlinux
-rwxrwxr-x 1 ubuntu ubuntu 578M Apr 15 08:14 vmlinux

?linux-4.9.301 ls -hl ./arch/x86_64/boot/bzImage
lrwxrwxrwx 1 ubuntu ubuntu 22 Apr 15 08:15 ./arch/x86_64/boot/bzImage -> ../../x86/boot/bzImage

?linux-4.9.301 ls -hl ./arch/x86/boot/bzImage
-rw-rw-r-- 1 ubuntu ubuntu 9.3M Apr 15 08:15 ./arch/x86/boot/bzImage



几种linux内核文件的区别:
vmlinux 编译出来的最原始的内核文件,未压缩。
zImage 是vmlinux经过gzip压缩后的文件。
bzImage bz表示“big zImage”,不是用bzip2压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个640K)。
bzImage解压缩内核到高端内 存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。
uImage U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的tag。
vmlinuz 是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接。
initrd 是“initial ramdisk”的简写。一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。


编译busyboxLinux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动。
而驱动又位于??/lib/modules??,得挂载根文件系统才能读取,这就陷入了一个两难境地,系统无法顺利启动。
于是有了initramfs根文件系统,其中包含必要的设备驱动和工具,bootloader加载initramfs到内存中,内核会将其挂载到根目录??/???,然后运行??/init??脚本,挂载真正的磁盘根文件系统。
这里借助BusyBox构建极简initramfs,提供基本的用户态可执行程序。
可以从??busybox官网地址??下载最新版本,或者直接使用wget下载我使用的版本。
wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
$ tar -xvf busybox-1.32.1.tar.bz2
$ cd busybox-1.32.1/
$ make menuconfig

在编译busybox之前,我们需要对其进行设置,执行make menuconfig,如下


这里一定要选择静态编译,编译好的可执行文件??busybox??不依赖动态链接库,可以独立运行,方便构建initramfs。
之后选择Exit退出,到这里我们就可以编译busybox了,执行下面的命令
make -j 8
# 安装完成后生成的相关文件会在 _install 目录下
make & & make install

构建initramfs根文件系统
[root@localhost temp]# ls
busybox-1.29.0busybox-1.29.0.tar.bz2
[root@localhost temp]# mkdir initramfs
[root@localhost temp]# cd initramfs
[root@localhost initramfs]# cp ../busybox-1.32.1/_install/* -rf ./
[root@localhost initramfs]# mkdir dev proc sys
[root@localhost initramfs]# sudo cp -a /dev/null,console,tty,tty1,tty2,tty3,tty4 dev/
[root@localhost initramfs]# rm -f linuxrc
[root@localhost initramfs]# vim init
[root@localhost initramfs]# chmod a+x init
[root@localhost initramfs]# ls
bindevinitprocsbinsysusr

其中init的内容如下
#!/bin/busybox sh
echo "==DBG== INIT SCRIPT"
mount -t proc none /proc
mount -t sysfs none /sys

echo -e "==DBG== Boot took $(cut -d-f1 /proc/uptime) seconds"
exec /sbin/init

打包initramfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
[root@localhost initramfs]# ls ../
busybox-1.29.0busybox-1.29.0.tar.bz2initramfsinitramfs.cpio.gz

安装QEMU
apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils

安装GDB
wget https://ftp.gnu.org/gnu/gdb/gdb-10.1.tar.gz
tar -xzvf gdb-10.1.tar.gz
cdgdb-10.1
./configure
# 必需要安装这两个库
sudo apt-get install texinfo
sudo apt-get install build-essential
make -j 8
sudo make install

QEMU启动调试内核
?linux-4.9.301 qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd ../initramfs.cpio.gz -append "nokaslr console=ttyS0" -s -S -nographic

  • ??-kernel ./arch/x86/boot/bzImage??:指定启用的内核镜像;
  • ??-initrd ../initramfs.cpio.gz??:指定启动的内存文件系统;
  • ??-append "nokaslr console=ttyS0"??? :附加参数,其中??nokaslr?? 参数必须添加进来,防止内核起始地址随机化,这样会导致 gdb 断点不能命中;
  • ??-s?? :监听在 gdb 1234 端口;
  • ??-S?? :表示启动后就挂起,等待 gdb 连接;
  • ??-nographic???:不启动图形界面,调试信息输出到终端与参数??console=ttyS0?? 组合使用;
在另一个窗口中,输入gdb,即可开启调试。
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
warning: Can not parse XML target description; XML support was disabled at compile time
Remote g packet reply is too long (expected 560 bytes, got 608 bytes): 0000000000000000000000000000000000000000000000006306000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0ff0000000000000200000000f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f0300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801f0000
(gdb) Remote debugging using localhost:1234
Undefined command: "Remote".Try "help".
(gdb) warning: Can not parse XML target description; XML support was disabled at compile timeQuit

但是,在启动GDP调试时报错了,在查阅了诸多资料后,很多博客都给出了修复方法:源码重新安装gdb,并修改gdb/remote.c文件的一段代码。但是我尝试了,发现行不通。
出现该问题的原因是:编译 的是64 位模式的内核代码,但是运行是在 32 位保护模式下。64 位代码将无法在该环境中正常运行。
终于在stackflow上找到了修复方法:具体可以参考下面两篇文章。
https://stackoverflow.com/questions/48620622/how-to-solve-qemu-gdb-debug-error-remote-g-packet-reply-is-too-long
https://wiki.osdev.org/QEMU_and_GDB_in_long_mode
【利用QEMU+GDB搭建Linux内核调试环境】文章中给出了三种修复方法,我这里只列出了一种,即修改GDB源码,重新编译安装。
--- gdb/remote.c2016-04-14 11:13:49.962628700 +0300
+++ gdb/remote.c2016-04-14 11:15:38.257783400 +0300
@@ -7181,8 +7181,28 @@
buf_len = strlen (rs-> buf);

/* Further sanity checks, with knowledge of the architecture.*/
+// HACKFIX for changing architectures for qemu. Its ugly. Dont use, unless you have to.
+// Just a tiny modification of the patch of Matias Vara (http://forum.osdev.org/viewtopic.php?f=13& p=177644)
if (buf_len > 2 * rsa-> sizeof_g_packet)
-error (_("Remote g packet reply is too long: %s"), rs-> buf);
+
+warning (_("Assuming long-mode change. [Remote g packet reply is too long: %s]"), rs-> buf);
+rsa-> sizeof_g_packet = buf_len ;
+
+for (i = 0; i < gdbarch_num_regs (gdbarch); i++)
+
+if (rsa-> regs[i].pnum == -1)
+continue;
+
+if (rsa-> regs[i].offset > = rsa-> sizeof_g_packet)
+rsa-> regs[i].in_g_packet = 0;
+else
+rsa-> regs[i].in_g_packet = 1;
+
+
+// HACKFIX: Make sure at least the lower half of EIP is set correctly, so the proper
+// breakpoint is recognized (and triggered).
+rsa-> regs[8].offset = 16*8;
+

/* Save the size of the packet sent to us by the target.It is used
as a heuristic when determining the max size of packets that the

cd gdb-10.1
./configure
make -j 8

    推荐阅读