前言
在 Linux 内核开发的征途上,调试往往被视为一道令人望而生畏的关卡。回顾过往,我们习惯了在充斥着复杂硬件依赖的环境中挣扎:要么面对着只有串口输出的“黑屏白字”,通过满屏滚动的 printk 日志大海捞针,试图从无数无关信息中捕捉那一丝错误的踪迹;要么只能在纯命令行界面的 GDB 中,机械地输入 list、print,在缺乏直观代码上下文的窘境下,凭借想象力去构建程序的执行路径。
这种传统的调试方式,不仅对开发者的经验与耐心提出了极高的要求,更因其低下的效率而成为技术探索的阻碍。每一次定位问题,都像是一场与未知的搏斗,繁琐且充满不确定性。
幸运的是,技术的进步为我们带来了新的可能。本文将带你跨越这一鸿沟,利用 QEMU 的系统模拟能力与 VSCode 强大的图形化调试界面,搭建一套现代化的 Linux 内核调试环境。我们将把那个只能在终端里敲命令的“远古时代”抛诸脑后,迎来一个支持源码级断点、变量实时查看、一键调用栈分析的图形化调试新纪元。
准备工作
这部分如果宿主机是windows系统,如果没有linux服务器作为内核编译的环境,我个人建议使用windows子系统WSL功能,安装可以百度进行。很方便也方便迁移。

只需要把这个文件拷贝到其他电脑,就可以完成迁移。
linux内核编译
安装linux内核编译工具
sudo apt-get install **gcc-8-aarch64-linux-gnu build-essential libncurses5-dev gdb-multiarch qemu-system-arm**
sudo apt-get install flex
sudo apt-get install bison
sudo apt-get install bc
sudo apt install libssl-dev
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu下载linux内核源码
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.1.80.tar.xz备注:源码的来源网络上很多,也可以在 https://www.kernel.org/ 下载
编译linux内核
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=out/ LLVM=1 defconfig
bear -- make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=out/ LLVM=1 -j32注意:演示指令中bear -- 是为了生compile_commands.json ,这个文件是为了在vscode中通clangd 插件实现内核函数跳转
制作根文件系统
下载busybox
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xvf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
开启静态binary
make menuconfig
Settings ---->
[*] Buidld Static binary (no shared libs)编译busybox
sudo apt install gcc-arm-linux-gnueabi
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j32
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- CONFIG_PREFIX=../rootfs/arm64/ install如果出现以下报错:
compilation
of tc.c fails with:
networking/tc.c: In function ‘cbq_print_opt’:
networking/tc.c:236:27: error: ‘TCA_CBQ_MAX’ undeclared (first use in this
function); did you mean ‘TCA_CBS_MAX’?
236 | struct rtattr *tb[TCA_CBQ_MAX+1];
| ^~~~~~~~~~~
| TCA_CBS_MAX解决方法:
把tc.c从networking文件夹里移出去即可。这是一个bug。见[Bug 15934] New: Busybox fails to build with linux kernels >= 6.8
制作虚拟硬盘
mkdir virtdisk
cd virtdisk
dd if=/dev/zero of=rootfs_ext4.img bs=1M count=1024
mkfs.ext4 rootfs_ext4.img
mkdir tmpfs
sudo mount -t ext4 rootfs_ext4.img tmpfs/ -o loop创建一些文件
创建fstab
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc$ ls
fstab init.d inittab profile
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc$ cat fstab
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans-virtio 0 0创建inittab
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh创建profile
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc$ cat profile
export HOSTNAME=virt-machine
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\#"
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH创建rcS
sudo mkdir -p /etc/init.d
touch /etc/init.d/rcS
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc$ cd init.d/
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc/init.d$ ls
rcS
ubuntu@sh-liuqiN:~/virtual_linux/virtdisk/tmpfs/etc/init.d$ cat rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -svscode配置
配置launch.json
VSCode 将作为 GDB 的前端界面。
安装插件:安装 C/C++ 扩展(由 Microsoft 提供)。
创建配置文件:在 VSCode 中打开内核源码文件夹,按下
F5或点击“运行和调试”,选择 C++ (GDB/LLDB),创建launch.json文件。配置内容:
program: 指向你的内核 ELF 文件(通常是源码根目录下的vmlinux,不是bzImage)。miDebuggerPath: 指向你的 GDB 路径(如/usr/bin/gdb或/usr/bin/gdb-multiarch)。miDebuggerServerAddress: 填写localhost:1234。
示例 launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "kernel debug",
"type": "cppdbg",
"request": "launch",
"program": "/home/ubuntu/virtual_linux/linux-5.15/out/vmlinux", // 例如 /home/user/linux/vmlinux
"miDebuggerServerAddress": "127.0.0.1:1234",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
}
]
}配置QEMU启动参数
通过命令行启动 QEMU,并让它暂停运行,等待 GDB 连接。
关键参数说明:
-s: 这是一个快捷参数,相当于-gdb tcp::1234,让 QEMU 在 1234 端口开启 GDB Server。-S: 冻结 CPU,直到 GDB 连接并发出“继续”指令。这对于调试内核启动过程非常有用。nokaslr: 在内核启动参数(-append)中加入此选项,关闭地址空间布局随机化,防止符号地址每次启动都变,导致断点失效。
示例启动脚本 run_qemu.sh :
# 虚拟linux和宿主机之间的文件传输靠这个
SHARE_DIR=./share
# 切不同的内核,只需要改这个
#KERNEL_IMAGE=/home/ubuntu/virtual_linux/linux-6.1.80/out/arch/arm64/boot/Image
KERNEL_IMAGE=/home/ubuntu/virtual_linux/linux-5.15/out/arch/arm64/boot/Image
qemu-system-aarch64 \
-machine virt,gic-version=3 \
-cpu cortex-a57 \
-display none \
-nographic \
-m 1024 \
-smp 4 \
-kernel $KERNEL_IMAGE \
--append "noinitrd root=/dev/vda rw console=ttyAMA0 loglevel=8 nokaslr" \
-drive if=none,file=./virtdisk/rootfs_ext4.img,id=hd0 \
-device virtio-blk-device,drive=hd0 \
--fsdev local,id=kmod_dev,path=$SHARE_DIR,security_model=none \
-device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount \
-s -S启动调试
首先运行
run_qemu.sh

此时,命令行终端会卡在此处,等待GDB连接
进入linux内核,按下F5进入调试模式

注意:如果没有设置断点,当启动调试模式时,虚拟linux将会立即启动并完成开机,命令行终端将立即输出日志。
设置断点

设置函数跳转
利用vscode的 clangd 插件以及 compile_commands.json 完成函数跳转功能
下载并安装插件

插件安装成功后,会在右下角弹出安装clangd工具(如果服务器中没有安装)
设置与C/C++插件的冲突解决
因为我们需要利用C/C++插件来进行内核调试,又得利用clangd插件进行函数跳转,但是两者又同时开启又会有冲突!所以使用下面的方式可以解决冲突。
新建 .vscode/settings.json 和前面的 launch.json 同目录
{
"clangd.path": "clangd",
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}",
"--background-index",
"--completion-style=detailed",
"--header-insertion=never",
"--query-driver=aarch64-linux-gnu-gcc"
],
"C_Cpp.intelliSenseEngine": "disabled",
"editor.suggest.snippetsPreventQuickSuggestions": false
}重启vscode,等待下一次启动时进行索引
索引结束后,我们就可以通过 ctrl + 鼠标左键,进行函数跳转了

总结
总而言之,掌握 QEMU 与 VSCode 的协同调试,意味着你拥有了一个强大而灵活的“内核显微镜”。希望这篇指南能帮助你顺利搭建起属于自己的调试环境,开启一段更高效、更愉悦的 Linux 内核开发与学习之旅。
我也建议我们在学习Linux内核的时候,要善于利用这种方式,做一些实验来验证猜想。比如我们在学习开源社区遇到的bug以及bufix时,就可以利用这种方式快速复现,以及快速验证bugfix,非常好用!