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:
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:
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:
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, 就可以对内核进行调试了.