Linux Kernel: build, boot and debug in QEMU

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

编译内核

拉取源码

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

调整配置

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

1
2
3
4
5
6
$ 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

编译

1
2
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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

1
2
3
4
5
6
7
8
9
$ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[    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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/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

然后使用如下命令:

1
2
3
4
5
6
7
8
9
10
11
$ 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, 会发现只加载了几个模块

1
2
3
4
5
6
[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 打包前将编译好的模块放入:

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

使用 gdb 调试内核

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

1
2
3
4
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