使用QEMU和GDB调试Linux内核
排查Linux内核Bug,研究内核机制,除了查看资料阅读源码,还可通过调试器,动态分析内核执行流程。
QEMU模拟器原生支持GDB调试器,这样可以很方便地使用GDB的强大功能对操作系统进行调试,如设置断点;单步执行;查看调用栈、查看寄存器、查看内存、查看变量;修改变量改变执行流程等。
编译调试版内核
对内核进行调试需要解析符号信息,所以得编译一个调试版内核。
$ cd linux-4.14 |
这里需要开启内核参数CONFIG_DEBUG_INFO
和CONFIG_GDB_SCRIPTS
。GDB提供了Python接口来扩展功能,内核基于Python接口实现了一系列辅助脚本,简化内核调试,开启CONFIG_GDB_SCRIPTS
参数就可以使用了。
Kernel hacking ---> |
构建initramfs根文件系统
Linux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动,而驱动又位于/lib/modules
,得挂载根文件系统才能读取,这就陷入了一个两难境地,系统无法顺利启动。于是有了initramfs根文件系统,其中包含必要的设备驱动和工具,boot loader加载initramfs到内存中,内核会将其挂载到根目录/
,然后运行/init
脚本,挂载真正的磁盘根文件系统。
这里借助BusyBox构建极简initramfs,提供基本的用户态可执行程序。
编译BusyBox,配置CONFIG_STATIC
参数,编译静态版BusyBox,编译好的可执行文件busybox
不依赖动态链接库,可以独立运行,方便构建initramfs。
$ cd busybox-1.28.0 |
Settings ---> |
$ make -j 20 |
会安装在_install
目录:
$ ls _install |
创建initramfs,其中包含BusyBox可执行程序、必要的设备文件、启动脚本init
。这里没有内核模块,如果需要调试内核模块,可将需要的内核模块包含进来。init
脚本只挂载了虚拟文件系统procfs
和sysfs
,没有挂载磁盘根文件系统,所有调试操作都在内存中进行,不会落磁盘。
$ mkdir initramfs |
init文件内容:
#!/bin/busybox sh |
打包initramfs:
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz |
调试
启动内核:
$ cd busybox-1.28.0 |
-s
是-gdb tcp::1234
缩写,监听1234端口,在GDB中可以通过target remote localhost:1234
连接;-kernel
指定编译好的调试版内核;-initrd
指定制作的initramfs;-nographic
取消图形输出窗口,使QEMU成简单的命令行程序;-append "console=ttyS0"
将输出重定向到console,将会显示在标准输出stdio。
启动后的根目录, 就是initramfs中包含的内容:
/ # ls |
由于系统自带的GDB版本为7.2,内核辅助脚本无法使用,重新编译了一个新版GDB:
$ cd gdb-7.9.1 |
启动GDB:
$ cd linux-4.14 |
使用内核提供的GDB辅助调试功能:
(gdb) apropos lx |
在函数cmdline_proc_show
设置断点,虚拟机中运行cat /proc/cmdline
命令即会触发。
(gdb) b cmdline_proc_show |
获取当前进程
《深入理解Linux内核》第三版第三章–进程,讲到内核采用了一种精妙的设计来获取当前进程。
Linux把跟一个进程相关的thread_info
和内核栈stack放在了同一内存区域,内核通过esp寄存器获得当前CPU上运行进程的内核栈栈底地址,该地址正好是thread_info
地址,由于进程描述符指针task字段在thread_info
结构体中偏移量为0,进而获得task。相关汇编指令如下:
movl $0xffffe000, %ecx /* 内核栈大小为8K,屏蔽低13位有效位。 |
指令运行后,p就获得当前CPU上运行进程的描述符指针。
然而在调试器中调了下,发现这种机制早已经被废弃掉了。thread_info
结构体中只剩下一个字段flags,进程描述符字段task已经删除,无法通过thread_info
获取进程描述符了。
而且进程的thread_info
也不再位于进程内核栈底了,而是放在了进程描述符task_struct
结构体中,见提交sched/core: Allow putting thread_info into task_struct和x86: Move thread_info into task_struct,这样也无法通过esp寄存器获取thread_info
地址了。
(gdb) p $lx_current().thread_info |
这样做是从安全角度考虑的,一方面可以防止esp寄存器泄露后进而泄露进程描述符指针,二是防止内核栈溢出覆盖thread_info
。
Linux内核从2.6引入了Per-CPU变量,获取当前指针也是通过Per-CPU变量实现的。
(gdb) p $lx_current().pid |
参考: