使用QEMU+GDB调试linux内核
Env:
Ubuntu 20.04 with kernel version 5.13.0-30-generic
gcc v9.3.0
compiling kernel v5.16
编译内核
1 | make menuconfig |
编译完成后的内核镜像为arch/x86_64/boot/bzImage
(big
zip Image).
准备根文件系统
内核需要一个根文件系统,initramfs本义是一个内存中的临时文件系统,它的主要使命就是挂载磁盘上的根文件系统,并具有完成此任务所需要的相关module。如果我们不需要持久化存储,也可以直接用initramfs作为根文件系统。
选项1:使用busybox创建initramfs
busybox将一些简化的常用UNIX工具集成进了一个可执行文件,可以用它构建最小化的rootfs.
配置和编译busybox,通过
make install
把busybox展开成UNIX目录结构,可以发现所有的工具都是到busybox的soft link.1
2
3
4
5
6make menuconfig
静态编译
Settings ->
[*]Build static binary (no shared libs)
make -j8
make install(重要)kernel默认使用
/init
作为一个进程,创建一个init
文件,让它去调有实现的/sbin/init
即可。顺便可以挂一下procfs和sysfs。1
2
3
4
5
6
7
8
9
10
11cd _install
cat > init << EEOOFF
!/bin/sh
/bin/mount -t proc none /proc
/bin/mount -t sysfs sysfs /sys
echo "Your own /init is used!"
exec /sbin/init
EEOOFF
chmod a+x init准备一些文件的空实现,否则内核引导时找不到这些文件会一直报错:
1
2
3
4
5mkdir proc dev sys
mkdir -p etc/init.d
touch etc/init.d/rcS
chmod a+x rcS
touch dev/{tty1,tty2,tty3,tty4}使用
cpio
打包成initrd.1
find . -print0 | cpio --null -ov --format=newc| gzip -9 > somewhere/initramfs.cpio.gz
这里得到的initramfs并没有承担挂载磁盘文件系统的责任,它自身就会成为kernel的根文件系统。
选项2(推荐):使用busybox创建boot floppy
我们也可以不使用initramfs
,而是使用busybox创建一个持久化的boot
floppy供kernel使用,在busybox-source-code/examples/bootfloppy
目录下有相关脚本及文档,大致步骤如下:
创建一个大小为1k*4000=4M的空镜像,并在其上构建文件系统。
1
2dd if=/dev/zero of=mybootfloppy bs=1k count=4000
mkfs.ext4 mybootfloppy将这个img作为回环设备挂载到文件系统,使我们可以像访问块设备一样访问它。
1
2sudo mkdir -p /mnt/mybootfloppy
sudo mount -o loop mybootfloppy /mnt/mybootfloppy在该img上安装(已经静态编译的)busybox.
1
sudo make CONFIG_PREFIX=/mnt/mybootfloppy install
创建最小文件系统所需的
dev
和proc
,将busybox提供的etc
下部分文件也放入img,解除挂载。1
2
3sudo mkdir /mnt/mybootfloppy/{dev,proc,etc}
sudo cp -r examples/bootfloppy/etc/* /mnt/mybootfloppy/etc
sudo umount /mnt/mybootfloppyetc
下的文件基本都是被init
程序使用的空实现。
使用QEMU运行kernel
参数说明:
-kernel
指定内核二进制文件-nographic
无图形界面,以命令行运行qemu-initrd
指定init ram disk,即装有initramfs的img文件-hda
设置虚拟硬盘0,并指定装入的img及相关参数-append
用于向kernel传入额外的参数-m
指定使用的内存大小-s
指定qemu在某端口监听,默认1234-S
使虚拟cpu在执行首条指令前暂停
注意:nokaslr
参数是必须的(如果编译内核时开启了kaslr),否则地址空间被随机化,gdb的断点无法生效。
基于initramfs
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage \ |
参数console=ttyS0
使我们有一个终端可以观察输出。
基于hard drive
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage \ |
额外的参数root
指定了根文件系统的位置。这种启动方式不需要initramfs,似乎是因为qemu承担了挂载硬盘的责任。
当然,也可以同时添加-initrd
和-hda
参数,如果initramfs会去加载硬盘文件系统的话(比如是由mkinitramfs
创建的),也能正常进入boot
floppy上的root file system.
对硬盘上的文件所做的更改会被持久化保存,不过rootfs默认会被只读挂载,可以通过mount -o remount rw /
重新挂载。
使用gdb调试kernel
因为编译内核时打开了调试选项和gdb支持,源码根目录下的vmlinux
文件已经包含了符号信息,此外也生成了便于调试的gdb脚本,在gdb中有很多命令可供使用,可用apropos word
查找与word有关的命令。
执行gdb vmlinux
会读入符号表并载入相关的script,如果被安全机制阻拦,可以在~/.gdbinit
中使用add-auto-load-safe-path
允许脚本执行。
令QEMU运行kernel(额外添加-s -S
参数),在gdb中连接到qemu:
1 | (gdb)target remote :1234 |
设置断点并运行,qemu在断点处暂停,符合预期。
1 | (gdb) b start_kernel |
在vscode中使用gdb调试kernel
配置launch.json
并在代码中设置断点即可。
1 | #launch.json |
讨论
- initrd/initramfs是可选而不是必需的,kernel镜像本身如果包含了所需的driver和module也可以直接从硬盘加载rootfs.[3]
- (未经考证)似乎基于kvm进行调试需要使用硬件断点
hbreak
,基于qemu使用break
就足够了。[2] - [5]指出mmu启动前的符号因为直接使用了物理地址,而无法被gdb定位并打断点,这个现象在xv6中也存在。目前未确定kernel被加载到的物理地址,暂未尝试给出的解决方案。