使用Qemu和GDB对Linux内核进行调试
使用Qemu对Linux内核进行调试是一种较为便捷的方式,近日进行了一番实践,并将大致步骤与其中一些小坑记录了下来。
环境
由于放长假赋闲在家,所以手头只有一台装有MacOS的MBP可用,而Linux内核的开发与调试使用Linux环境下会比较方便,所以就使用VMware Fusion创建了一台安装有Ubuntu 18.04系统的虚拟机。由于编译Linux内核及相关软件需要的资源较多,所以为虚拟机配置了双核CPU、2GB内存和20GB磁盘空间(笔记本本身资源有限),但实际使用(特别是物理内存和硬盘)捉襟见肘,于是又在系统中添加了3GB的SWAP内存并扩容了20GB的磁盘空间(其实还是不太够)才解决问题。
编译Linux内核
首先,尝试对内核进行编译,在编译前需要使用通过KConfig启动内核的调试配置。
下载内核源码
由于Linux内核代码量非常大,且由于国内网络大家都懂的原因,所以的下载内核源码是一项较为复杂的体力活动。
第一种方法是直接Clone Linux源码的Git仓库,当前,其仓库大约为3.7GB。在通过内核官网(https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/)或者GitHub(https://github.com/torvalds/linux)进行Clone的过程中,经常会遇到连接断开的情况,非常捉急。而如果通过国内镜像源,如清华Kernel Git镜像进行Clone的时候,最开始速度飞快,但是后面速度会越来越慢。因此,如果不像我这样头铁的话,不建议使用这样的方式下载Kernel的源码。
另一种较为简单的方式是下载特定版本的源码,这些源码的tarball包可以从内核官网或者镜像站获得。我在实验中使用的内核版本为4.19,gz压缩包的大小约为150MB。
配置内核
如果是使用Git Clone的方式获取的内核源码,需要通过git checkout v4.19
将内核源码置位4.19版本。
在编译之前,首先需要安装相关的依赖(如果提示缺少其它依赖按需安装即可)
1 | sudo apt install libncurses5-dev libssl-dev bison flex libelf-dev gcc make openssl libc6-dev |
在编译之前,需要使用KConfig对内核编译选项进行配置,在内核文件夹下,使用make menuconfig
(命令行界面)或make gconfig
(基于gtk的图形化界面)对内核进行配置。在配置时,需要打开如下选项:
1 | Kernel hacking -> Kernel debugging |
并保证如下选项没有开启:
1 | Kernel hacking -> Compile time checks and compiler options -> Reduce debugging information |
在退出配置后,可以发现内核目录中生成了一个名为.config
的配置文件。
编译内核
配置完成后,就可以使用make
编译内核,在多核CPU中可以使用make -jx
启动多线程编译(x为启动的线程数)。
如果一切正常,在漫长的等待后,内核将编译完成。编译会在内核根目录下生成vmlinux
文件,它是编译出的原始内核文件(含有调试信息),而会在arch/x86/boot/bzImage
目录下生成压缩后的内核文件(当然是在编译的体系结构为x86的情况下)。
编译安装GDB和Qemu
由于内核调试所需的GDB和Qemu版本可能会比apt源中的版本高,所以,最好自行编译安装这些软件。
编译安装GDB
首先,从官网(http://www.gnu.org/software/gdb/download/)下载GDB的源码并解压(这里使用的是官网中最新的GDB 9.1),需要注意的是,网上有些博客中提到需要修改GDB的源码,其实是不必要的,报错的原因是没有自动检测到目标体系结构的类型,所以只需设置该类型即可。
解压后进入GDB文件夹,执行下列指令,即可完成编译安装:
1 | mkdir build |
最后,通过使用gdb -v
确定gdb的版本是否为9.1,如果是,则说明安装成功。
编译安装Qemu
首先,从官网下载(https://www.qemu.org/download/#source)Qemu的源码并解压(这里使用的是Qemu 5.0.0)。
由于在Ubuntu GUI中使用Qemu还需要多媒体图形库SDL,所以需要首先使用apt安装sdl:
1 | sudo apt install libsdl2-2.0-0 libsdl2-dev libsdl2-gfx-1.0-0 libsdl2-gfx-dev libsdl2-image-2.0-0 libsdl2-image-dev |
进入Qemu目录后,执行./configure
检查系统配置并生成Makefile,需要注意检查的时候是否检测到了SDL的支持,其输出的部分内容如下所示:
1 | profiler no |
然后执行make && make install
即可完成Qemu的编译与安装。
在安装完Qemu后,会生成如qemu-xxx
和qemu-system-xxx
的一系列命令,用于仿真不同体系结构的用户态应用和操作系统,可以通过如qemu-system-x86_64 --version
命令确认Qemu是否安装成功。
制作ROOTFS
在内核启动后需要一个带有init程序的rootfs,所以在调试内核前需要制作一个rootfs。
构建基于initrd的rootfs
initrd是一种位于内存的根文件系统,它可以在硬盘被驱动之前载入系统。这里为了方便,只将一个简单的程序写入initrd,并将其作为init程序(即系统启动后的第一个用户态进程)。除此之外,也可以使用busybox作为initrd中的init程序。
创建一下简单的c程序,命名为fakeinit.c
。
1 |
|
然后使用gcc编译这段代码,在编译的时候需要使用静态链接,并且如果如果在配置内核的时候没有启用64位支持(64-bit kernel),则需要将代码编译为32位程序,方法是在gcc命令行中添加-m32
选项。
编译命令如下:
1 | gcc --static -o fakeinit fakeinit.c |
在编译后,使用cpio程序进行打包:
1 | echo fakeinit | cpio -o --format=newc > initrd_rootfs.img |
这样,一个基于initrd的rootfs即制作完成。
构建基于硬盘镜像的rootfs
这里使用busybox构建基于硬盘镜像的rootfs。其中,busybox是一个集成了数百个Linux常用命令和工具的单个软件,在对内核进行测试的时候非常方便,号称“The Swiss Army Knife of Embedded Linux”。
下载编译busybox
首先,从官网(https://busybox.net/downloads/)下载busybox的源码并解压(这里使用的是最新的busybox-1.31.1)。
在解压并进入busybox文件夹后,首先使用make gconfig
或make menuconfig
对其进行配置,需要启用如下选项:
1 | Settings -> Build Options -> Build static binary (no shared libs) |
如果需要将其编译为32位版本,则需要将-m32
命令填入如下选项:
1 | Settings -> Build Options -> Additional CFLAGS |
与内核相同,在退出后,会在目录中生成一个名为.config
的配置文件。
然后,使用make
命令编译busybox。
使用busybox创建rootfs
首先,创建一个空的磁盘镜像文件,然后将其格式化:
1 | dd if=/dev/zero of=./busybox_rootfs.img bs=1M count=10 |
然后,挂载刚刚创建的磁盘镜像(需要使用loop设备):
1 | mkdir rootfs_mount |
接着,在busybox源码目录中,将编译好的busybox目标文件安装到rootfs文件夹:
1 | make install CONFIG_PREFIX=/path/to/rootfs_mount/ |
最后,配置busybox的init,并卸载rootfs:
1 | mkdir /path/to/rootfs_mount/proc |
现在,一个基于busybox的rootfs磁盘镜像就制作成功了。
使用Qemu和GDB调试内核
使用Qemu启动内核
由于编译的内核体系结构为x86,所以使用qemu-system-x86_64
程序来载入并启动内核。
如果使用intird作为rootfs,则具体命令为:
1 | qemu-system-x86_64 \ |
如果使用磁盘镜像作为rootfs,则具体命令为:
1 | qemu-system-x86_64 \ |
使用GDB调试内核
最后一步,由于刚刚Qemu开启了远程调试,所以只需要将gdb通过连入即可:
1 | gdb ./linux/vmlinux # 指定调试文件为包含调试信息的内核文件 |
如果此时直接在gdb调试器中使用target remote:1234
连入Qemu的gdb server,则会出现报错Remote ‘g’ packet reply is too long
,这是由于gdb没有正确识别调试目标的体系结构造成的(有些博客认为需要修改源代码屏蔽这个错误,实际上是不必要的),所以只需要在远程attach之前使用set arch i386:x86-64:intel
设置目标体系结构即可。
例如,你希望在start_kernel函数设置断点进行调试,则在启动Qemu后,gdb的命令如下:
1 | gdb ~/linux/vmlinux |
可以发现,内核在启动后被中断在start_kernel函数上。
后记
内核文档
在内核的文档中,有一篇详细讲解了如何使用GDB调试内核。
该文档的最新版本可见于内核的官网:https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html。
而具体的版本就需要在内核源码中编译文档了,例如html版本的文档可以使用make htmldocs
进行编译,在启动HTTP服务器后,可以在浏览器中进行访问,例如,http://127.0.0.1:8000/dev-tools/gdb-kernel-debugging.html。
参考来源
本文参考了两篇较为优质的博客: