Linux Kernel: build, boot and debug in QEMU

一次内核编译到运行的尝试

编译内核

拉取源码

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

调整配置

为了让 gdb 可以调试内核并加载符号,需要更改一些编译选项.

$ make menuconfig
Kernel hacking  --->
	[*] Kernel debugging
	Compile-time checks and compiler options  --->
		Debug information (Rely on the toolchain's implicit default DWARF version)
		[*] Provide GDB scripts for kernel debugging

编译

$ make -j$(nproc)
$ make bzImage

内核启动流程

我们并不需要弄清楚启动过程的所有细节, 这里只简略介绍以便于解释后文的操作

Kernel Initialization

这个阶段内核会将映像自解压到内存中,并且进行硬件相关的初始化, 设置中断处理 (interrupt handling)、内存管理 (memory management), 挂载根文件系统 (/) 或者初始 RAM 磁盘 (initrd), 加载驱动.

Init process

内核初始化后,就会准备我们所熟悉的用户空间 (Userspace) 的初始化.

事实上内核会执行 /sbin/init, /etc/init 或 initramfs/initrd 中指定的第一个用户空间程序 (通常是 Systemd、SysVinit 或 OpenRC), 作为用户空间中的第一个进程 (PID=1), 当然它可以是任何可执行的文件,包括 shell script.

使用 QEMU 运行内核

构建 rootfs

根文件系统遵循 https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard, 这里使用 Arch Linux 的 pacstrap 来构建一个较简单的 rootfs:

#!/bin/bash

set -e

ROOTFS_DIR="test_kernel/arch_full_rootfs"

if [ -d "$ROOTFS_DIR" ]; then
    sudo rm -rf "$ROOTFS_DIR"
fi

mkdir -p "$ROOTFS_DIR"

sudo pacstrap -K -c "$ROOTFS_DIR" \
    base \
    linux-firmware \
    bash \
    coreutils \
    util-linux \
    procps-ng \
    iproute2 \
    iputils \
    net-tools \
    vim \
    nano \
    less \
    grep \
    sed \
    gawk \
    tar \
    gzip \
    which \
    man-db \
    man-pages

sudo arch-chroot "$ROOTFS_DIR" passwd -d root

sudo tee "$ROOTFS_DIR/init" > /dev/null << 'EOF'
#!/bin/bash

mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
mount -t tmpfs tmp /tmp
mkdir -p /dev/pts /dev/shm
mount -t devpts devpts /dev/pts
mount -t tmpfs shm /dev/shm

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export HOME=/root
export TERM=linux

if [ -x /usr/lib/systemd/systemd-udevd ]; then
    /usr/lib/systemd/systemd-udevd --daemon 2>/dev/null
    udevadm trigger --action=add 2>/dev/null
    udevadm settle 2>/dev/null
fi

clear

exec /bin/bash --login
EOF

sudo chmod +x "$ROOTFS_DIR/init"

sudo mkdir -p "$ROOTFS_DIR"/{dev,proc,sys,run,tmp}

然后使用如下脚本将其打包为 ext4 image:

#!/bin/bash

set -e

ROOTFS_DIR="test_kernel/arch_full_rootfs"
IMAGE_FILE="test_kernel/arch_full.ext4"
IMAGE_SIZE="4G"  

if [ ! -d "$ROOTFS_DIR" ]; then
    exit 1
fi

if [ -f "$IMAGE_FILE" ]; then
    rm -f "$IMAGE_FILE"
fi

dd if=/dev/zero of="$IMAGE_FILE" bs=1M count=0 seek=$(echo $IMAGE_SIZE | sed 's/G/*1024/;s/M//;' | bc) status=progress

mkfs.ext4 -F -L "arch-rootfs" "$IMAGE_FILE"

MOUNT_POINT=$(mktemp -d)
sudo mount -o loop "$IMAGE_FILE" "$MOUNT_POINT"

sudo cp -a "$ROOTFS_DIR/"* "$MOUNT_POINT/"

sync
sudo umount "$MOUNT_POINT"
rmdir "$MOUNT_POINT"

sudo chown $USER:$USER "$IMAGE_FILE"

我们尝试使用如下参数运行 QEMU

$ KERNEL="arch/x86/boot/bzImage"
$ ROOTFS="test_kernel/arch_full.ext4"
$ qemu-system-x86_64 \
  -kernel "$KERNEL" \
  -drive file="$ROOTFS",format=raw,if=virtio \
  -m 4G \
  -smp 4 \
  -append "console=ttyS0" \
  -nographic

预期下,会产生 Kernel panic:

[    1.983880] /dev/root: Can't open blockdev
[    1.987750] List of all bdev filesystems:
[    1.987876]  fuseblk
[    1.987891] 
[    1.988146] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[    1.988715] CPU: 1 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.18.0-rc3 #5 PREEMPT(voluntary)  2b0b48d497e5105aac88eb0a7903527369b0379b
[    1.989240] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Arch Linux 1.17.0-2-2 04/01/2014
[    1.989766] Call Trace:
[    1.989950]  <TASK>
[    1.990291]  dump_stack_lvl+0x5d/0x80
[    1.990581]  vpanic+0xdb/0x2d0
[    1.990658]  panic+0x6b/0x6b
[    1.990732]  mount_root_generic+0x1cf/0x270
[    1.990903]  prepare_namespace+0x1dc/0x230
[    1.991032]  kernel_init_freeable+0x282/0x2b0
[    1.991163]  ? __pfx_kernel_init+0x10/0x10
[    1.991305]  kernel_init+0x1a/0x140
[    1.991398]  ret_from_fork+0x1c2/0x1f0
[    1.991483]  ? __pfx_kernel_init+0x10/0x10
[    1.991592]  ret_from_fork_asm+0x1a/0x30
[    1.991723]  </TASK>
[    1.993053] Kernel Offset: disabled
[    1.993394] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---

这是因为 ext4 相关的功能编译为了内核模块,而且并没有加载进内核.

借助 initramfs, 我们可以先在其中加载 ext4 相关模块, 然后再挂载真正的根文件系统.

构建 initramfs

使用如下脚本构建一个实验性的 initramfs:

#!/bin/bash
set -e

INITRAMFS_DIR="test_kernel/initramfs"
INITRAMFS_FILE="test_kernel/initramfs.cpio.gz"
KERNEL_VERSION=$(make kernelrelease 2>/dev/null)

if [ -d "$INITRAMFS_DIR" ]; then
    rm -rf "$INITRAMFS_DIR"
fi

mkdir -p "$INITRAMFS_DIR"/{bin,sbin,etc,proc,sys,dev,newroot,lib/modules}

cp /bin/busybox "$INITRAMFS_DIR/bin/"
chmod +x "$INITRAMFS_DIR/bin/busybox"

cd "$INITRAMFS_DIR/bin"
for cmd in sh mount umount mkdir mknod switch_root insmod modprobe cat echo sleep ls; do
    ln -sf busybox $cmd
done
cd - > /dev/null

MODULE_DIR="$INITRAMFS_DIR/lib/modules/$KERNEL_VERSION"
mkdir -p "$MODULE_DIR/kernel/fs/ext4"
mkdir -p "$MODULE_DIR/kernel/fs/jbd2"
mkdir -p "$MODULE_DIR/kernel/fs/mbcache"
mkdir -p "$MODULE_DIR/kernel/crypto"

for module in ext4 jbd2 mbcache crc16; do
    MODULE_PATH=$(find . -name "${module}.ko" -o -name "${module}.ko.gz" -o -name "${module}.ko.xz" | head -1)
    if [ -n "$MODULE_PATH" ]; then
        echo "  找到: $module -> $MODULE_PATH"
        cp "$MODULE_PATH" "$MODULE_DIR/"
    else
        echo "  警告: 未找到 $module 模块"
    fi
done

cd "$INITRAMFS_DIR"
cat > "lib/modules/$KERNEL_VERSION/modules.dep" << EOF
kernel/fs/ext4/ext4.ko: kernel/fs/jbd2/jbd2.ko kernel/fs/mbcache/mbcache.ko
kernel/fs/jbd2/jbd2.ko: kernel/crypto/crc16.ko
kernel/fs/mbcache/mbcache.ko:
kernel/crypto/crc16.ko:
EOF

touch "lib/modules/$KERNEL_VERSION/modules.order"
cd - > /dev/null

cat > "$INITRAMFS_DIR/init" << 'INIT_SCRIPT'
#!/bin/sh

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

sleep 1

echo "加载内核模块..."
insmod /lib/modules/*/crc16.ko 2>/dev/null || echo "  crc16 已加载或不需要"
insmod /lib/modules/*/mbcache.ko 2>/dev/null || echo "  mbcache 已加载或不需要"
insmod /lib/modules/*/jbd2.ko 2>/dev/null || echo "  jbd2 已加载或不需要"
insmod /lib/modules/*/ext4.ko 2>/dev/null || echo "  ext4 已加载或不需要"

echo ""
echo "内核模块加载完成"
echo ""

if [ ! -b /dev/vda ]; then
    echo "错误: 根设备 /dev/vda 不存在"
    echo "可用的块设备:"
    ls -l /dev/vd* 2>/dev/null || echo "  未找到 virtio 块设备"
    echo ""
    echo "启动 shell 进行调试..."
    exec /bin/sh
fi

echo "挂载根文件系统 /dev/vda..."
mount -t ext4 /dev/vda /newroot

if [ $? -ne 0 ]; then
    echo "错误: 无法挂载根文件系统"
    echo ""
    echo "启动 shell 进行调试..."
    exec /bin/sh
fi

echo "根文件系统挂载成功"
echo ""

if [ ! -x /newroot/init ]; then
    echo "错误: /newroot/init 不存在或不可执行"
    echo ""
    echo "启动 shell 进行调试..."
    umount /newroot
    exec /bin/sh
fi

umount /proc
umount /sys
umount /dev

exec switch_root /newroot /init
INIT_SCRIPT

chmod +x "$INITRAMFS_DIR/init"

cd "$INITRAMFS_DIR"
find . -print0 | cpio --null --create --format=newc 2>/dev/null | gzip -9 > "../$(basename $INITRAMFS_FILE)"
cd - > /dev/null

然后使用如下命令:

$ KERNEL="arch/x86/boot/bzImage"
$ INITRAMFS="test_kernel/initramfs.cpio.gz"
$ ROOTFS="test_kernel/arch_full.ext4"
$ qemu-system-x86_64 \
  -kernel "$KERNEL" \
  -initrd "$INITRAMFS" \
  -drive file="$ROOTFS",format=raw,if=virtio \
  -m 4G \
  -smp 4 \
  -append "console=ttyS0" \
  -nographic

加载内核模块

在成功启动的 QEMU 环境内输入 lsmod, 会发现只加载了几个模块

[root@archlinux /]# lsmod
Module                  Size  Used by
ext4                 1159168  1
jbd2                  200704  1 ext4
mbcache                20480  1 ext4
crc16                  12288  1 ext4

在 Linux 中, 内核模块存放在 /usr/lib/modules/*kernel_release*/ 位置. 我们需要在 rootfs 打包前将编译好的模块放入:

$ make modules_install INSTALL_MOD_PATH=/home/summer/git/linux/test_kernel/arch_full_rootfs

使用 gdb 调试内核

在 QEMU 的启动命令添加下列参数:

qemu-system-x86_64 \
  ... \
  -s \ #run a gdb server at tcp::1234
  -S   #pause simulator until a `continue` from gdb

kernel 在编译时提供了带符号的内核文件 vmlinux 和一个 gdb 脚本 vmlinux-gdb.py

加载该文件然后连接 QEMU, 就可以对内核进行调试了.

gdb attach
start_kernel

References

  1. https://en.wikipedia.org/wiki/Booting_process_of_Linux#Kernel
  2. https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard
  3. https://wiki.archlinux.org/title/Kernel_module