1

使用 Qemu 和 GDB 对 Linux 内核进行调试

 3 years ago
source link: https://blog.zhengzi.me/debug-linux-kernel-with-qemu-and-gdb/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 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 版本。

在编译之前,首先需要安装相关的依赖(如果提示缺少其它依赖按需安装即可)

sudo apt install libncurses5-dev libssl-dev bison flex libelf-dev gcc make openssl libc6-dev

在编译之前,需要使用 KConfig 对内核编译选项进行配置,在内核文件夹下,使用 make menuconfig(命令行界面)或 make gconfig(基于 gtk 的图形化界面)对内核进行配置。在配置时,需要打开如下选项:

Kernel hacking -> Kernel debugging
Kernel hacking -> KGDB:kernel debugger
Kernel hacking -> Compile time checks and compiler options -> Provide GDB scripts for kernel debugging

并保证如下选项没有开启:

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 文件夹,执行下列指令,即可完成编译安装:

mkdir build 
cd build
../configure
make -j4
sudo make install

最后,通过使用 gdb -v 确定 gdb 的版本是否为 9.1,如果是,则说明安装成功。

编译安装 Qemu

首先,从官网下载 (https://www.qemu.org/download/#source) Qemu 的源码并解压(这里使用的是 Qemu 5.0.0)。

由于在 Ubuntu GUI 中使用 Qemu 还需要多媒体图形库 SDL,所以需要首先使用 apt 安装 sdl:

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 的支持,其输出的部分内容如下所示:

profiler          no
static build no
SDL support yes (2.0.8)
SDL image support yes
GTK support no
GTK GL support no
VTE support no
TLS priority NORMAL

然后执行 make && make install 即可完成 Qemu 的编译与安装。

在安装完 Qemu 后,会生成如 qemu-xxxqemu-system-xxx 的一系列命令,用于仿真不同体系结构的用户态应用和操作系统,可以通过如 qemu-system-x86_64 --version 命令确认 Qemu 是否安装成功。

制作 ROOTFS

在内核启动后需要一个带有 init 程序的 rootfs,所以在调试内核前需要制作一个 rootfs。

构建基于 initrd 的 rootfs

initrd 是一种位于内存的根文件系统,它可以在硬盘被驱动之前载入系统。这里为了方便,只将一个简单的程序写入 initrd,并将其作为 init 程序(即系统启动后的第一个用户态进程)。除此之外,也可以使用 busybox 作为 initrd 中的 init 程序。

创建一下简单的 c 程序,命名为 fakeinit.c

#include <stdio>
int main()
{
printf("hello world!");
printf("hello linux!");
printf("hello world!");
printf("hello linux!");
fflush(stdout);
while(1);
return 0;
}

然后使用 gcc 编译这段代码,在编译的时候需要使用静态链接,并且如果如果在配置内核的时候没有启用 64 位支持(64-bit kernel),则需要将代码编译为 32 位程序,方法是在 gcc 命令行中添加 -m32 选项。

编译命令如下:

gcc --static -o fakeinit fakeinit.c
gcc --static -o fakeinit fakeinit.c -m32 (编译为32位可执行程序)

在编译后,使用 cpio 程序进行打包:

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 gconfigmake menuconfig 对其进行配置,需要启用如下选项:

Settings -> Build Options -> Build static binary (no shared libs)

如果需要将其编译为 32 位版本,则需要将 -m32 命令填入如下选项:

Settings -> Build Options -> Additional CFLAGS
Settings -> Build Options -> Additional LDFLAGS

与内核相同,在退出后,会在目录中生成一个名为.config 的配置文件。

然后,使用 make 命令编译 busybox。

使用 busybox 创建 rootfs

首先,创建一个空的磁盘镜像文件,然后将其格式化:

dd if=/dev/zero of=./busybox_rootfs.img bs=1M count=10
mkfs.ext3 ./busybox_rootfs.img

然后,挂载刚刚创建的磁盘镜像(需要使用 loop 设备):

mkdir rootfs_mount
sudo mount -t ext3 -o loop ./busybox_rootfs.img ./rootfs_mount

接着,在 busybox 源码目录中,将编译好的 busybox 目标文件安装到 rootfs 文件夹:

make install CONFIG_PREFIX=/path/to/rootfs_mount/

最后,配置 busybox 的 init,并卸载 rootfs:

mkdir /path/to/rootfs_mount/proc
mkdir /path/to/rootfs_mount/dev
mkdir /path/to/rootfs_mount/etc
cp busybox-source-code/examples/bootfloppy/* /path/to/rootfs_mount/etc/
sudo umount /path/to/rootfs_mount

现在,一个基于 busybox 的 rootfs 磁盘镜像就制作成功了。

使用 Qemu 和 GDB 调试内核

使用 Qemu 启动内核

由于编译的内核体系结构为 x86,所以使用 qemu-system-x86_64 程序来载入并启动内核。

如果使用 intird 作为 rootfs,则具体命令为:

qemu-system-x86_64 \
-kernel ./linux/arch/x86/boot/bzImage \ # 指定编译好的内核镜像
-initrd ./rootfs/initrd_rootfs.img \ # 指定rootfs
-serial stdio \ #指定使用stdio作为输入输出
-append "root=/dev/ram rdinit=/fakeinit console=ttyS0 nokaslr" \ # 内核参数,指定使用initrd作为rootfs,禁止地址空间布局随机化
-s -S # 指定Qemu在启动时暂停并启动gdb server,等待gdb的连入(端口默认为1234)

如果使用磁盘镜像作为 rootfs,则具体命令为:

qemu-system-x86_64 \
-kernel ./linux/arch/x86/boot/bzImage \
-hda ./rootfs/busybox_rootfs.img \ # 指定磁盘镜像
-serial stdio \
-append "root=/dev/sda console=ttyS0 nokaslr" \ # 内核参数,指定root磁盘,禁止地址空间布局随机化
-s -S

使用 GDB 调试内核

最后一步,由于刚刚 Qemu 开启了远程调试,所以只需要将 gdb 通过连入即可:

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 的命令如下:

gdb ~/linux/vmlinux
(gdb) set arch i386:x86-64:intel
(gdb) add-auto-load-safe-path ~/linux
(gdb) target remote:1234
(gdb) b start_kernel
(gdb) c

可以发现,内核在启动后被中断在 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

本文参考了两篇较为优质的博客:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK