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

Posted LinuxDriverDev

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用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"勾选:

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

利用QEMU+GDB搭建Linux内核调试环境_嵌入式硬件_02

以上配置完成后会在当前目录生成 ​​.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能够接管并继续引导的状态。


编译busybox

Linux系统启动阶段,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,如下

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

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

这里一定要选择静态编译,编译好的可执行文件​​busybox​​不依赖动态链接库,可以独立运行,方便构建initramfs。

之后选择Exit退出,到这里我们就可以编译busybox了,执行下面的命令

make -j 8
# 安装完成后生成的相关文件会在 _install 目录下
make && make install

构建initramfs根文件系统

[root@localhost temp]# ls
busybox-1.29.0 busybox-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
bin dev init proc sbin sys usr

其中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.0 busybox-1.29.0.tar.bz2 initramfs initramfs.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
cd gdb-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

文章中给出了三种修复方法,我这里只列出了一种,即修改GDB源码,重新编译安装。

--- gdb/remote.c     2016-04-14 11:13:49.962628700 +0300
+++ gdb/remote.c 2016-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
使用 GDB + Qemu 调试 Linux 内核

vscode + qemu + gdb 搭建内核开发环境

linux kernel pwn notes(内核漏洞利用总结)

构建调试Linux内核网络代码的环境MenuOS系统

构建调试Linux内核网络代码的环境MenuOS系统

使用QEMU调试Linux内核代码