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

举报
嵌入式与Linux那些事 发表于 2022/04/19 22:22:49 2022/04/19
【摘要】 前言对用户态进程,利用gdb调试代码是很方便的手段。而对于内核态的问题,可以利用crash等工具基于coredump文件进行调试。其实我们也可以利用一些手段对Linux内核代码进行gdb调试,qemu就是一种。qemu是一款完全软件模拟(Binary translation)的虚拟化软件,在虚拟化的实现中性能相对较差。但利用它在测试环境中gdb调试Linux内核代码,是熟悉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能够接管并继续引导的状态。

编译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,如下

这里一定要选择静态编译,编译好的可执行文件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): 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. 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 < 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
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 <http://gnu.org/licenses/gpl.html>
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:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "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 打了两个断点, 两个断点都成功命中了。

本文参考

https://www.shuzhiduo.com/A/kjdw2a2q5N/
https://cloud.tencent.com/developer/article/1793157
https://blog.csdn.net/alexanderwang7/article/details/113180447
https://blog.csdn.net/sjc2870/article/details/122017247
https://stackoverflow.com/questions/8662468/remote-g-packet-reply-is-too-long
https://stackoverflow.com/questions/4943857/linux-kernel-live-debugging-how-its-done-and-what-tools-are-used/42316607#42316607

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。