良许Linux教程网 干货合集 利用QEMU+GDB调试Linux内核

利用QEMU+GDB调试Linux内核

前言

对于用户态进程,使用gdb进行代码调试是一种非常方便的方法。而对于内核态问题,我们可以利用一些工具如crash,基于coredump文件进行调试。

实际上,我们也可以通过一些手段来对Linux内核代码进行gdb调试,其中一个常用的方式是使用qemu。

qemu是一款全软件模拟(二进制翻译)的虚拟化软件,其虚拟化实现在性能方面相对较差。但是,利用qemu在测试环境中进行gdb调试Linux内核代码,是熟悉Linux内核代码的一种好方法。通过这种方式,我们可以在虚拟环境中模拟运行Linux内核,并使用gdb来调试代码。

本文实验环境:

  • 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”勾选:

image-20230820235722829
image-20230820235722829
image-20230820235725743
image-20230820235725743

以上配置完成后会在当前目录生成 .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,如下

image-20230820235729375
image-20230820235729375
image-20230820235732207
image-20230820235732207

这里一定要选择静态编译,编译好的可执行文件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.29.0/_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):
 0000000000000000000000000000000000000000000000006306000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000f0ff0000000000000200000000f00000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000010000060000000000000000

(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. It's ugly. Don't 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 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
sudo make install

接着就可以敲gdb 启动调试。

➜  linux-4.9.301 gdb                                                                                                                             
GNU gdb (GDB) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
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-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
    .

For helptype "help".
Type "apropos word" to search for commands related to "word".
(gdb) file vmlinux
Reading symbols from vmlinux...
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
warning: Can not parse XML target description; XML support was disabled at compile time
warning: Assuming long-mode change. [Remote 'g' packet reply is too long: PU]
0x000000000000fff0 in exception_stacks ()
(gdb) break start_kernel
Breakpoint 1 at 0xffffffff81fc6a95: file init/main.c, line 486.
(gdb) break  rest_init
Breakpoint 2 at 0xffffffff818aa1e1: file init/main.c, line 385.
(gdb) c
Continuing.

Breakpoint 1, start_kernel () at init/main.c:486
486             set_task_stack_end_magic(&init_task);
(gdb) c
Continuing.

Breakpoint 2, rest_init () at init/main.c:385
385     {
(gdb) 

在start_kernel 和 rest_init 打了两个断点, 两个断点都成功命中了。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部