用QEMU来调试内核 -- 亲身体验篇

本文记录了用QEMU构建最小Linux系统并进行调试的过程。

修改:
v1 2015-10-04 原稿
v2 2016-02-07 将initrd启动替换成硬盘启动

愿景

我在用QEMU来调试内核 – 亲身体验篇中大致记录了邮件列表上和网上搜索到的内核调试方法,并没有完全进行验证。今天亲自实践了一番,发现:

  • QEMU调试果然爽快
  • 调试环境搭建过程中有很多细节需要注意

先来看看有了QEMU内核调试环境后,我调试内核的大致步骤。

  • 修改内核,make编译(不用完整执行,生成bzImage就可以中断make)。执行

    qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage \    # 指定编译好的内核
    -hda rootfs.img \        # 指定硬盘
    -append "root=/dev/sda"      # 告诉内核硬盘上有根文件系统
    
  • 运行GDB(这个GDB是要自己编译的,见上篇文章的末尾)。

    gdb vmlinux
    > target remote localhost:1234
    
  • 进入弹出的QEMU窗口,CTRL+ALT+2进入QEMU控制台,输入gdbserver

  • 调试开始!

上述步骤针对图形模式,如果工作在字符界面下,第一步的命令需要替换成:

qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage \
 -hda rootfs.img \
 -append "root=/dev/sda console=ttyS0"

最后添加的console=ttyS0把QEMU的输入输出定向到当前终端上,即让我们可以使用执行qemu-system-x86命令的终端操作虚拟机中的系统,而不需要开启qemu的GUI窗口。不过这样上述第三步进入QEMU控制台的方式就不好使了。如果需要控制台,那么第一步命令需要替换成:

qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage \
 -hda rootfs.img \
 -append "root=/dev/sda console=ttyS0" -monitor stdio

这样一来,QEMU控制台的输入输出就定向到当前终端上了,不过同时我们无法利用该终端操作虚拟机中的系统了。两全其美的方法还有待探寻。

再也不用像使用VMware那样等一万年去make modules&&make install &&make modules_install以及费尽心思去调教grub了。节省了大量时间,同时资源占用也少太多。一切都是整整齐齐的模样。

上面就是我们的目标啦。怎么才能实现呢?分析上述qemu-system-x86_64的参数,我们需要为它准备两样东西:内核镜像bzImage和rootfs。这两个东西构成了一个刚好可用的运行系统。

为什么要构建自己的运行系统

插一段话,解释什么是LFS。LFS是Linux From Scratch缩写,意思是利用网络上的开源代码,从头构建Linux发行版。“构建”这里指编译和安装,不是指设计程序和敲代码的过程。构建的目标Linux发行版,不是让你从头写Linux内核本身。Linux发行版有很多外延,比如CentOS、Ubuntu、Arch等等。我自个儿就琢磨着呗,我们做内核开发,不能老用别人的发行版:一来我们要分清什么是Linux的共性特征,什么是一些发行版加进去的个性特征;二来从头构建Linux发行版能加深我们对这个系统的理解。更重要的是,毕竟我们研究内核其实就是在溯源,我不希望在探求原理路途上有什么迷雾遮住自己的眼,而是喜欢“一切都在掌握之中”的良好感觉。

调试环境的搭建

内核编译我想不必再说了。使用默认配置文件即可。唯一需要注意的是,为了避免交叉编译,我们最后使用与自己正在使用的机器架构一致的配置。例如,我的机器是64位x86,所以我使用下面的命令:

make x86_64_defconfig

然后再用make menuconfig命令配置一下DEBUG_INFO。最后make即可得到bzImage文件。

现在我们还需要准备一个rootfs.img文件,这是一个装有根文件系统的磁盘镜像文件,里面有一些像shell之类的二进制工具,让操作系统能够和用户交互。更重要的是,根文件系统往往还包含init程序,正是它完成了系统初始化的工作。那么如何制作这个rootfs.img呢?

第一步,创建一个磁盘镜像文件并格式化之:

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

第二步,下载、配置、编译busybox。配置时需要注意的是,busybox采用静态链接会减去很多麻烦。具体说在 make menuconfig时,勾选:

BusyboxSettings->Build options->BuildBusybox as a static binary

第三步,安装busybox到rootfs文件夹:

mkdir /path/to/rootfs
sudo mount -t ext3 -o loop rootfs.img /path/to/rootfs
# busybox源代码目录中执行
make install CONFIG_PREFIX=/path/to/rootfs

第四步,配置busybox的init:

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

rootfs.img准备完成了!一切都准备就绪,快试试吧!

附:initrd方式的启动

您现在正在阅读的这篇文章是多次修改后的版本。在原版中,我们的环境搭建可不是用了rootfs.img这种快捷简单的构建方式,而是采用了initrd的方式。这部分内容舍不得删 initrd功不可没,留在这里提供给需要的人看。

initrd是个啥呢?首先它也是一种根文件系统,不过它存在于内存中,而不是硬盘上。它的存在是为了解决这个问题:根文件系统存在于硬盘上,内核可能需要加载一些支持模块才能使用硬盘,而这些模块又是存放在根文件系统上,这样问题就死锁了。initrd全称initial RamDisk,它是一个用内存虚拟出的磁盘,不依赖具体的磁盘硬件,因此是通用的,既不需要特殊的支持模块。这样,上述过程就变成了:临时的根文件系统存在于initrd中,这个临时根文件系统里包含一些必要模块驱动真正的根文件系统所在的磁盘,用这些模块挂载真正的根文件系统,最后一个change root操作,真正的根文件系统顺利上位,而initrd退休。

有两种方式获得这个initrd。一种方式是在内核编译前生成,另一种通过内核编译选项在内核编译后生成。

方法一:编译前生成

先用下面的命令下载并解压缩BusyBox:

curl http://busybox.net/downloads/busybox-1.23.2.tar.bz2 | tar xjf -

为BusyBox创建工作目录:

mkdir -pv obj/busybox-x86
make O=obj/busybox-x86 defconfig

使用menuconfig配置BusyBox:

make O=obj/busybox-x86 menuconfig

选择静态链接:

-> Busybox Settings
    -> Build Options
    [ ] Build BusyBox as a static binary (no shared libs)

编译、安装BusyBox:

cd obj/busybox-x86
make -j2
make install

拷贝安装目录中的工具到initramfs目录中,这个文件夹就是日后的initramfs:

mkdir -p initramfs/x86-busybox
cd initramfs/x86-busybox
mkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin}}
cp -av obj/busybox-x86/_install/* ./

initramfs目录中没有init脚本,这可不行,我们的内核起来以后运行什么程序呢?我们手动创建一个:

#!/bin/sh

/bin/mount -t proc none /proc
/bin/mount -t sysfs sysfs /sys
echo 'Enjoy your new system!'
/bin/sh

生成initramfs:

find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs-busybox-x86.cpio.gz

得到initramfs了,任务结束了!看明白了吗?

同时这里给出生成initramfs的逆操作--拆分initramfs:

cpio -i -d -H newc -F initramfs_data.cpio --no-absolute-filenames

方法二:编译后生成

通过内核编译前配置CONFIG_INITRAMFS_SOURCE选项到一个存在的gzipped的initramfs、或是一个准initramfs目录、或是如下格式指定initramfs结构的txt文件:

dir /dev 755 0 0
nod /dev/console 644 0 0 c 5 1
nod /dev/loop0 644 0 0 b 7 0
dir /bin 755 1000 1000
slink /bin/sh busybox 777 0 0
file /bin/busybox initramfs/busybox 755 0 0
dir /proc 755 0 0
dir /sys 755 0 0
dir /mnt 755 0 0
file /init initramfs/init.sh 755 0 0

参考

如果你想弄清楚内核的初始化过程,这里有一份阅读列表:

How to Build a Custom Linux Kernel for Qemu

Linux From Scratch

Kernel Doc:Ramfs, Rootfs and Initramfs

Kernel Doc: Early Userspace Support

Embedded Linux From Scratch… in 40 minutes