简介

这篇文章介绍了如何通过qemu和gdb来调试内核

参考

https://www.bilibili.com/video/BV1Gd4y1C73i/?spm_id_from=333.788&vd_source=75dbec7ad4709dbb9145a059a5374980

感谢

Photo by Gordon Bishop from Pexels

准备

内核镜像

编译的时候开启CONFIG_DEBUG_INFO

位置

kernel hacing/Compile-time checks and compiler options/compile the kernel with debug info

pic1

编译之后,需要生成的两个文件:vmlinux和System.map

其中,vmlinux是elf可执行文件,map是符号映射表。vmlinux是gdb要调试的文件,而map是查阅的表

1
2
3
4
5
6
7
8
[liode@liodePC:60:linux-5.15.79]$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=34e258a3ee6609b274e448f3dc6e61a02ac2971e, with debug_info, not stripped
[liode@liodePC:63:linux-5.15.79]$ cat System.map |head -n 5
0000000000000000 D __per_cpu_start
0000000000000000 D fixed_percpu_data
00000000000001de A kexec_control_code_size
0000000000001000 D cpu_debug_store
0000000000002000 D irq_stack_backing_store

gdb

gdb不需要什么设置

qemu

qemu启动的时候需要加入一些参数,其中一些的含义

cmdline: nokaslr 禁用内核地址空间布局随机

-S在开始时阻塞CPU执行

-s开启GDB服务器,端口1234

-gdb tcp::1234 开启gdb服务器,端口可以自己指定

makefile内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.PHONY: initramfs
initramfs:
cd initramfs_d && find . -print0|cpio -ov --null --format=newc|gzip -9 >../initramfs.img
.PHONY: run
run:
qemu-system-x86_64\
-kernel bzImage\
-smp 2\
-initrd initramfs.img\
-m 1G\
-nographic\
-S\
-gdb tcp::9000\
-append "earlyprintk=serial,ttyS0 console=ttyS0 nokaslr"
.PHONY: clean
clean:
rm initramfs.img

调试过程

原理

说一下原理,在qemu启动过程中,因为加入了-S参数,会阻塞cpu,也就是会卡住,同时,因为通过-gdb tcp::9000这个参数,如果启动gdb,就可以通过9000端口来调试这个内核了

在gdb这边,运行的是vmlinux这个elf文件,在里面配置一下9000端口,就远程调试qemu启动的这个内核了,接下来,gdb就全权接管了qemu的内核,可以通过设置断点,单步运行等功能进行调试了

实验

qemu这边的准备

qemu这边使用make run,之后会卡住,输出如下

[liode@liodePC:137:qemu]$ make run

1
2
3
4
5
6
7
8
9
qemu-system-x86_64\
-kernel bzImage\
-smp 2\
-initrd initramfs.img\
-m 1G\
-nographic\
-S\
-gdb tcp::9000\
-append "earlyprintk=serial,ttyS0 console=ttyS0 nokaslr"

gdb这边的准备

gdb这边,执行gdb vmlinux,首先会读取符号表,然后会停住,等待我们输入命令,输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[liode@liodePC:69:linux-5.15.79]$ gdb vmlinux
GNU gdb (GDB) Fedora Linux 12.1-6.fc37
Copyright (C) 2022 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-redhat-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"...
Reading symbols from vmlinux...
(gdb)

这个时候,就可以和qemu那边建立关系了,配置同样的端口

1
2
3
4
5
6
7
8
9
(gdb) target remote : 9000
Remote debugging using : 9000
warning: Remote gdbserver does not support determining executable automatically.
RHEL <=6.8 and <=7.2 versions of gdbserver do not support such automatic executable detection.
The following versions of gdbserver support it:
- Upstream version of gdbserver (unsupported) 7.10 or later
- Red Hat Developer Toolset (DTS) version of gdbserver from DTS 4.0 or later (only on x86_64)
- RHEL-7.3 versions of gdbserver (on any architecture)
0x000000000000fff0 in exception_stacks ()

这个时候就已经建立关系了,可以开始正常的调试过程了,只不过我们调试不是一个应用层的程序,而是一个运行在qemu模拟出来的硬件上面的linux内核程序就是了,所以才会如此的麻烦了,哈哈哈,这还真是有趣呢

正常调试开始

可以尝试下在start_kernel这个函数这里打一个端点,这个函数是内核启动的过程中很早的一个函数

1
2
(gdb) break start_kernel
Breakpoint 1 at 0xffffffff836c7f0a: file init/main.c, line 936.

如果去查看kernel的源码的话,就会发现,正好是在init/main.c line936的位置了,gdb的行为和我们预期的完全一致!这实在是太好了

1
2
3
4
asmlinkage__visiblevoid__init__no_sanitize_addressstart_kernel(void)
{
char*command_line;
char*after_dashes;

那这个地址对不对呢,如何判断?这就要用到map文件了,就是编译出来的那个System.map,用vim搜索一下看看

pic2

也是完全正确的

这个时候,qemu那边还因为-S参数的原因卡住呢,gdb执行continue就会解除这种阻塞,让程序执行下去,而刚才又在start_kernel函数这里打上了断点,因此,只要输入continue,就会运行到这里停住,就和平时的调试完全相同

输入continue之后,gdb这边的输出如下

1
2
3
4
5
(gdb) continue
Continuing.

Thread 1 hit Breakpoint 1, start_kernel () at init/main.c:936
936 {

表示停在这一行了

再次输入continue,内核就会启动完全,我们就可以在qemu那边正常使用了

当然,其他的gdb命令,单步运行等等都可以尝试,就和应用层的调试一样了,同时,还可以通过符号表来查看有哪些函数

总结

这篇文章介绍了如何通过qemu和gdb工具来调试内核,首先要编译内核,记得勾选保留调试信息,然后qemu那边要配置一些入口参数,让cpu阻塞,才方便观察内核的启动过程。同时配置gdb的调试端口。gdb这边就简单了,只需要对编译出来的vmlinux程序进行调试,然后配置和qemu那里一样的端口,之后就是正常的调试流程了

遗留的问题

gdb调试的是vmlinux这个程序,而qemu运行的内核是编译出的bzImage,这两个是怎么通过端口建立起来关系的呢?

qemu的入口参数-S阻塞cpu和cmdline: nokaslr 禁用内核地址空间布局随机到底是什么意思呢?

如何知道内核启动的时候到底是执行了那些函数?源码过于庞大,难道只能一步一步的观察吗?

现在还是命令行的方式,能不能集成到ide里,vscode或者clion等?