0%

Debug Linux Kernel by Qemu and GDB

1. 编译内核

1.1 配置选项

不同的架构有不同的默认文件,比如x86平台,可以在arch/x86/configs找到相关文件:

1
2
3
4
5
6
7
# junyu @ junyu in ~/Documents/linux/arch/x86/configs on git:136057256686 o [15:56:45] 
$ ll
total 24K
-rw-rw-r-- 1 junyu junyu 5.9K 11月 30 14:24 i386_defconfig #32位系统
-rw-rw-r-- 1 junyu junyu 147 11月 30 14:24 tiny.config
-rw-rw-r-- 1 junyu junyu 5.8K 11月 30 14:24 x86_64_defconfig #64位系统
-rw-rw-r-- 1 junyu junyu 744 11月 30 14:24 xen.config

我们首先根据我们的架构生成默认的配置,以下两个命令皆可:

1
2
$ make x86_64_defconfig
$ make defconfig

运行完毕后会在src目录下生成.config文件,然后我们需要对这个config文件进行微调,目的是开启Complie the kernel with debug info选项,如果要让内核可以进行调试,需要配置一些选项:

1
$ make menuconfig
1
2
3
4
5
Kernel hacking  --->
[*] Kernel debugging
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info
[*] Provide GDB scripts for kernel debugging

并且下面的选项一定要关闭,否则会打断点失败:

1
2
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR)

1.2 编译

1
$ make -j8

编译完成后,会生成一些文件,其中比较重要的当然是我们的内核文件:

  • src/arch/x86/boot/bzImage : 相当于/boot目录下vmlinuz,是一个压缩的,可以bootable的Linux kernel文件
  • src/vmlinux :一个非压缩的,不可以bootable的Linux kernel文件。是用来生成bzImage/vmlinuz的中间步骤。

完成后当然就是安装,但我们这里并不是真的要将本机的内核换掉,接下来的过程就交给 QEMU 了。

2. 构建initrd文件

关于initrd文件的具体说明请参考下文的“关于initramfs的一些说明“。

  1. Linux系统启动时使用initramfs (initram file system), initramfs可以在启动早期提供一个用户态环境,借助它可以完成一些内核在启动阶段不易完成的工作。

  2. initramfs打包的时候,要求打包成压缩的cpio档案。cpio档案可以嵌入到内核image中,也可以作为一个独立的文件在启动的过程中被GRUB load。

我们可以自己制作一个initramfs文件,参考Initramfs 原理和实践,在这里,我们使用busybox来制作initramfs。

2.1 准备

统一下目录,把环境变量TOP设置为这个目录

1
2
TOP=/home/junyu/Documents/linux-learn
cd $TOP

2.2 下载和解压BusyBox

1
2
curl https://busybox.net/downloads/busybox-1.34.1.tar.bz2 -o busybox-1.34.1.tar.bz2
tar -xf busybox-1.34.1.tar.bz2

2.3 configure busybox

1
2
3
4
cd $TOP/busybox-1.27.2
mkdir -pv ../obj/busybox-x86
make O=../obj/busybox-x86 defconfig
make O=../obj/busybox-x86 menuconfig

type /, search for “static”. You’ll see that the option is located at:

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

选择yes。

2.4 build busybox

1
2
3
cd ../obj/busybox-x86
make -j8
make install

2.5 build the directory structure for our initramfs

1
mkdir -pv $TOP/initramfs/x86-busyboxcd $TOP/initramfs/x86-busyboxmkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin}}cp -av $TOP/obj/busybox-x86/_install/* .

vim init 填入如下内容:

1
#!/bin/shmount -t proc none /procmount -t sysfs none /sysecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"exec /bin/sh
1
chmod +x initfind . -print0 \    | cpio --null -ov --format=newc \    | gzip -9 > $TOP/initramfs-busybox-x86.cpio.gz

We now have a minimal userland in $TOP/obj/initramfs-busybox-x86.cpio.gz that we can pass to qemu as an initrd (using the -initrd option).

3. 安装Qemu

1
$ sudo apt-get install qemu

qemu主要选项解释:

  • -s-gdb tcp::1234 的缩写,QEMU 监听在 TCP 端口 1234,等待 gdb 的连接。
  • -S:在启动时冻结 CPU,等待 gdb 输入 c 时继续执行。
  • -kernel:指定内核。
  • -initrd:指定 initramfs。
  • nographic:禁用图形输出并将串行 I/O 重定向到控制台。
  • -append "console=ttyS0:所有内核输出到 ttyS0 串行控制台,并打印到终端。

4. 利用gdb进行内核调试

在启动gdb调试之前,我们在用户目录~下创建一个.gdbinit文件,加入以下内容:

1
add-auto-load-safe-path ./scripts/gdb/vmlinux-gdb.py

4.1 启动脚本

1
2
3
4
5
#!/bin/bash
qemu-system-x86_64 -kernel bzImage \
-initrd initramfs-busybox-x86.cpio.gz \
-s -S \
-append "console=ttyS0" -nographic

4.2 启动gdb调试

在linux的源码目录下执行:

1
gdb -ex "target remote localhost:1234" vmlinux

然后就可以进行调试工作了

关于initramfs的一些说明

什么是ramdisk(内存磁盘)

  • ramdisk基于内存的块设备,具备块设备的一切属性
  • 一旦创建,大小固定,不可动态调整
  • 伪块设备,存在于内存中,与真实块设备基本无异,访问时候居然还需要缓存(都已经在内存中了还缓存个鸡儿)
  • 2.4及更早版本使用ramdisk构建initrd

什么是initrd(基于ramdisk的临时文件系统)

  • Boot loader Init Ram disk缩写,是一种机制,装载一个临时根文件系统到内存中
  • 作为Linux startup process的一部分,为实际根文件系统的加载做准备。
  • 对于2.4或更早的kernel来说,使用的是该方法。

什么是ramfs(基于内存缓存的文件系统)

  • Ramfs 是一个空间大小动态可变的基于 RAM 的文件系统,它是Linux 用来实现磁盘缓存(page cache and dentry cache)的文件系统。
  • ramfs 是一个仅存在与内存中文件系统,它没有后备存储(例如磁盘),也就是说 ramfs 文件系统所管理的文件都是存放在内存中,而不存放在磁盘中,如果计算机掉电关闭,那么 ramfs 文件系统中所有文件也就都没有了。
  • 当普通磁盘中的文件被操作系统加载到内存中时,内核会分配 page 来存储文件中的内容,然后进程通过读写内存中文件对应的 page 实现对文件的读写修改操作,当完成了所有的读写操作之后,文件对应的 page 就会被标记为脏页,然后在合适的时机被操作系统写回到原来的磁盘中对应的文件中,内存中原来存放这些文件的 page 就会被标记为干净,最后被系统回收重新使用。而 ramfs 文件系统中的文件当同样被加载到内存中 page 进行读写操作之后,它对应的 page 并不会被标记为脏页,因为 ramfs 中文件没有下级的后备存储器(例如,磁盘),也就没有了写回后备存储器的操作,所以为它分配的这些 page 也就无法回收了。
  • ramfs是一种统称,不一定就只是作为临时根文件系统,还可以作为其他用途,有其他特定命名。
  • ramdisk是一种基于ram的块设备,ramfs是一种基于ram的文件系统
  • initrd是init ramdisk的缩写,initramfs是init ramfs的缩写

什么是initramfs(基于ramfs的临时文件系统)

  • initramfs 是一种以 cpio 格式压缩后的 rootfs 文件系统,它通常和 Linux 内核文件一起被打包成 boot.img 作为启动镜像
  • BootLoader 加载 boot.img,并启动内核之后,内核接着就对 cpio 格式的 initramfs 进行解压,并将解压后得到的 rootfs 加载进内存,最后内核会检查 rootfs 中是否存在 init 可执行文件(该 init 文件本质上是一个执行的 shell 脚本),如果存在,就开始执行 init 程序并创建 Linux 系统用户空间 PID 为 1 的进程,然后将磁盘中存放根目录内容的分区真正地挂载到 / 根目录上,最后通过 exec chroot . /sbin/init 命令来将 rootfs 中的根目录切换到挂载了实际磁盘分区文件系统中,并执行 /sbin/init 程序来启动系统中的其他进程和服务。
  • 基于ramfs开发initramfs,取代了initrd

什么是initrd(多个含义)

  • initrd代指内核启动过程中的一个阶段,临时挂载文件系统,加载硬盘的基础驱动,进而过渡到最终的根文件系统
  • 早期基于ramdisk生成的临时根文件系统的名称
  • 现阶段虽然基于initramfs,但是临时根文件系统也依然存在某些发行版称其为initrd
  • CentOS 临时根文件系统命名为 initramfs-uname -r.img
  • Ubuntu 临时根文件系统命名为 initrd-uname -r.img

什么是tmpfs

  • ramfs 中有一个非常大的缺点就是你可以持续不断地向 ramfs 文件系统中的文件持续不断地写入数据直到填满整个物理内存空间为止。出现这个问题的原因就是前面介绍的 ramfs 文件系统不存在向普通磁盘那样的将内存中的文件内容写回到文件的操作,也就导致了它所占据的那部分内存空间是无法被释放的,正是因为这个原因,通常只有 root 用户才有读写 ramfs 文件系统中文件的权限。
  • tmpfs 文件系统是从 ramfs 衍生而来的一个文件系统,但是它相对于 ramfs 多了空间容量大小限制,并且还可以将文件系统中一些不必要的的文件内容写到交换空间中(swap space)。并且 tmpfs 文件系统的大小还可以通过 mount -o remount … 命令来重新调整。

什么是rootfs?

  • rootfs(也叫根文件系统) 它本质上就是一个 Linux 系统中基本的文件目录组织结构,也就是 Linux 系统中 / 根目录下的结构

解压initramfs到rootfs

  • “内核将Bootloader加载到内存中的initramfs中的文件解压到rootfs中”这句话的意思,Bootloader,比如grub,配置文件中的菜单项,每一个菜单项包括两个内容,一个是内核,一个是initrd。bootloader将initrd加载到内存,解压的工作是内核自己干的,所以initrd无法正常使用的话应该检查内核而不是grub

参考文献

  1. 在qemu环境中用gdb调试Linux内核
  2. Initramfs 原理和实践
  3. 在qemu上运行BusyBox