使用QEMU搭建Linux内核开发调试环境:从编译到GDB源码级调试

发布时间:2026/8/13 3:55:48
使用QEMU搭建Linux内核开发调试环境:从编译到GDB源码级调试 在 Linux 内核开发与调试的征途中你是否曾因缺少真实的硬件环境而苦恼或者你是否想在不重启物理机的情况下快速验证一个内核模块的改动又或者你是否对操作系统底层如何与硬件交互充满好奇却苦于没有安全的实验平台如果你有以上任一困惑那么 QEMU 将是你的得力助手。本文将从 Linux 内核开发者的视角出发系统性地讲解如何将 QEMU 打造为一个强大、灵活的内核开发与调试沙箱。我们将从核心概念入手逐步搭建环境编写并运行一个最简单的内核模块最终深入到使用 GDB 进行源码级单步调试。无论你是刚接触内核的新手还是希望优化现有工作流的资深开发者都能从中获得一套可直接复用的完整方案。1. 背景与核心概念为什么内核开发者需要 QEMU在深入实操之前我们有必要厘清 QEMU 是什么以及它为何能成为内核开发的“神器”。1.1 QEMU 是什么QEMU 是一个开源的、通用的机器模拟器和虚拟化器。简单来说它可以模拟一整套计算机硬件如 CPU、内存、磁盘、网卡等并在此虚拟硬件上运行未经修改的操作系统。对于内核开发者而言我们主要利用其系统模拟System Emulation功能。这意味着 QEMU 可以模拟一个完整的、从加电启动开始的计算机系统这正是运行一个完整 Linux 内核所需要的环境。与 VMware、VirtualBox 等基于宿主操作系统内核功能的“虚拟化”软件不同QEMU 的纯模拟模式不依赖宿主机的特定 CPU 虚拟化扩展如 Intel VT-x 或 AMD-V它通过二进制翻译来模拟指令执行。这带来了无与伦比的灵活性你可以在 x86 主机上运行为 ARM、RISC-V 或 MIPS 编译的内核反之亦然。这种跨架构模拟能力对于进行嵌入式 Linux 或新硬件架构适配的内核开发至关重要。1.2 QEMU 对内核开发的核心价值安全与隔离内核开发涉及系统底层错误的代码可能导致系统崩溃、数据丢失。在 QEMU 虚拟机中测试相当于在一个“沙箱”中操作无论内核如何崩溃宿主机的系统都安然无恙。快速迭代无需准备多台物理机也无需反复重启宿主机。修改内核代码后只需重新编译内核镜像然后在 QEMU 中启动新的虚拟机即可整个过程可能只需几十秒。精确的调试控制QEMU 内置了对 GDB 调试协议的支持。这意味着你可以像调试用户态程序一样使用 GDB 对正在运行的内核进行源码级单步调试、设置断点、查看变量和内存。这是物理机上极难实现的。灵活的硬件配置你可以轻松地为虚拟机配置特定的内存大小、CPU 核心数、磁盘映像、网络拓扑甚至模拟特定的硬件设备如自定义的 PCI 设备这对于驱动开发非常有用。可复现的环境通过保存一个磁盘镜像和启动命令你可以精确复现一个开发环境方便团队共享和问题追溯。1.3 核心组件关系梳理在后续的实践中我们会涉及几个核心组件它们的关系如下宿主系统Host你实际使用的物理机运行着如 Ubuntu、Fedora 等发行版。QEMU运行在宿主系统上的模拟器软件。客户机系统Guest在 QEMU 中运行的虚拟计算机。Linux 内核镜像bzImage/vmlinux被加载到客户机内存中并运行的核心程序。根文件系统Rootfs包含/bin,/sbin,/lib等目录的文件系统映像为客户机内核提供用户空间环境如init进程、shell。GDB运行在宿主机上的调试器通过 QEMU 提供的调试接口连接到客户机中运行的内核。理解了这些概念我们就可以开始动手搭建环境了。2. 环境准备与版本说明我们的目标是在一个 Linux 宿主机上使用 QEMU 启动一个同样为 Linux 的客户机并在其中运行我们开发的内核。以下是本次实践的环境概览。宿主机环境操作系统Ubuntu 22.04 LTS (x86_64)。其他主流发行版如 Fedora、CentOS 步骤类似。QEMU 版本本文使用qemu-system-x86_64版本 6.2.0。安装命令会获取你发行版仓库中的稳定版本。内核源码我们将使用 Linux 内核的稳定版本linux-5.15作为示例。你可以从 kernel.org 下载。构建工具链gcc,make,flex,bison等。调试器gdb。根文件系统构建工具busybox。我们用它来制作一个极简的根文件系统。版本兼容性说明 不同版本的 QEMU 和 Linux 内核在特性支持上可能有细微差别。本文选择的5.15是一个长期支持LTS版本与主流 QEMU 版本兼容性良好。如果你的项目要求特定版本请替换对应的版本号核心操作流程是通用的。3. 搭建基础开发环境3.1 安装 QEMU 及必要工具首先在 Ubuntu 宿主机上安装 QEMU 和编译内核所需的工具。# 更新软件包列表 sudo apt update # 安装 QEMU 系统模拟器 (x86_64架构) sudo apt install qemu-system-x86 qemu-system-gui -y # 安装内核编译依赖 sudo apt install build-essential libncurses-dev libssl-dev bc libelf-dev flex bison -y # 安装调试工具 GDB sudo apt install gdb -y # 安装用于制作根文件系统的 busybox 和 文件系统工具 sudo apt install busybox-static genext2fs -y安装完成后可以验证 QEMU 是否安装成功qemu-system-x86_64 --version命令应输出类似QEMU emulator version 6.2.0的信息。3.2 获取并配置 Linux 内核源码我们下载并解压内核源码并进行最基础的配置。# 1. 下载 Linux 5.15 内核源码 (你也可以使用其他版本此处以5.15.148为例) wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.148.tar.xz # 或者使用国内镜像如清华镜像https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.15.148.tar.xz # 2. 解压源码 tar -xvf linux-5.15.148.tar.xz cd linux-5.15.148 # 3. 生成默认配置 # 为 x86_64 架构生成一个默认的配置。这会创建 .config 文件。 make x86_64_defconfig # 4. 关键启用内核调试信息 # 我们需要在内核中嵌入调试符号否则 GDB 无法进行源码级调试。 # 执行 menuconfig 图形化配置界面 make menuconfig在make menuconfig界面中使用键盘方向键导航确保以下选项被启用标有[*]或*Kernel hacking-Compile-time checks and compiler options-Compile the kernel with debug info(CONFIG_DEBUG_INFO)同样在Kernel hacking下确保KGDB: kernel debugger是启用的。保存并退出通常按右方向键选择Save然后Exit。3.3 制作简易根文件系统 (Rootfs)内核启动后需要挂载一个根文件系统里面至少需要有一个init程序。我们使用busybox来制作一个极简的initramfs内存文件系统。# 1. 回到工作目录创建一个用于构建 rootfs 的文件夹 cd .. mkdir rootfs cd rootfs # 2. 创建基本的文件系统目录结构 mkdir -p bin sbin etc proc sys usr/bin usr/sbin dev home lib lib64 # 3. 将 busybox 安装到当前目录 # busybox 是一个集成了上百个常用命令的单一可执行文件。 busybox --install -s . # 4. 创建 init 脚本这是内核启动后运行的第一个用户空间进程 cat init EOF #!/bin/sh # 挂载虚拟文件系统 mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev # 打印欢迎信息 echo echo Hello from QEMU Linux Kernel Developer! echo # 启动一个交互式 shell exec /bin/sh EOF # 5. 赋予 init 脚本执行权限 chmod x init # 6. 将当前目录打包成 initramfs 镜像 # find . | cpio -o -H newc | gzip ../initramfs.cpio.gz # 更推荐使用以下命令避免包含上层目录路径 find . -print0 | cpio --null -ov --formatnewc | gzip -9 ../initramfs.cpio.gz cd ..现在我们得到了一个关键的文件initramfs.cpio.gz。它是一个被压缩的、包含完整目录结构和busybox工具的内存文件系统镜像。4. 编译内核并首次启动4.1 编译 Linux 内核回到内核源码目录开始编译。这个过程可能需要一些时间取决于你的 CPU 性能。cd linux-5.15.148 # 使用多线程编译以加快速度-j$(nproc) 表示使用与CPU核心数相同的线程数。 make -j$(nproc)编译成功后在arch/x86/boot/目录下会生成压缩的内核镜像bzImage在源码根目录下会生成带有完整调试符号的内核文件vmlinux。bzImage: 用于引导启动的压缩内核镜像。vmlinux: 原始的、未压缩的、包含所有符号的内核可执行文件是 GDB 调试所必需的。4.2 使用 QEMU 启动自定义内核现在使用 QEMU 启动我们刚刚编译的内核和制作的根文件系统。# 在 linux-5.15.148 目录的同级目录下执行 qemu-system-x86_64 \ -kernel ./linux-5.15.148/arch/x86/boot/bzImage \ -initrd ./initramfs.cpio.gz \ -append consolettyS0 nokaslr \ -nographic \ -s参数解析-kernel path: 指定要启动的内核镜像 (bzImage) 路径。-initrd path: 指定初始内存磁盘 (initramfs) 路径。-append “…”: 传递给内核的命令行参数。consolettyS0: 将控制台重定向到串口 0这样-nographic模式下我们才能看到输出。nokaslr:至关重要禁用内核地址空间布局随机化。如果不禁用GDB 设置的断点地址会因为每次启动的随机偏移而失效。-nographic: 不使用图形化窗口所有输出输入通过当前终端进行。非常适合在服务器或 SSH 会话中使用。-s:调试关键参数这是-gdb tcp::1234的简写表示在 TCP 的 1234 端口开启一个 GDB 调试服务器等待 GDB 连接。执行命令后QEMU 会启动虚拟机并输出一系列内核启动日志最后会看到我们init脚本打印的欢迎信息并进入busybox提供的shshell。你可以在这里执行一些简单的命令如ls,ps。要退出 QEMU可以按CtrlA然后松开再按X。5. 使用 GDB 进行内核源码级调试这是 QEMU 为内核开发者提供的“杀手级”功能。我们将演示如何连接 GDB设置断点并单步执行内核代码。5.1 启动 QEMU 并等待 GDB 连接首先以前台方式启动 QEMU并让它暂停在启动的最开始等待调试器连接。qemu-system-x86_64 \ -kernel ./linux-5.15.148/arch/x86/boot/bzImage \ -initrd ./initramfs.cpio.gz \ -append consolettyS0 nokaslr \ -nographic \ -S \ -s注意这里多了一个-S参数。它告诉 QEMU 在启动 CPU 前先冻结暂停直到 GDB 发送continue命令。现在 QEMU 窗口会卡住没有任何输出。5.2 在另一个终端中启动 GDB打开一个新的终端窗口切换到内核源码目录。cd linux-5.15.148 gdb vmlinux这会启动 GDB 并加载带有完整符号的vmlinux文件。在 GDB 界面中执行以下命令(gdb) target remote localhost:1234这条命令让 GDB 连接到 QEMU 在localhost:1234端口开放的调试服务。连接成功后GDB 会显示类似Remote debugging using localhost:1234的信息并可能显示当前暂停的地址如0x000000000000fff0这是复位向量地址。5.3 设置断点并调试现在我们可以像调试普通程序一样调试内核了。例如我们想在start_kernel函数这是内核 C 语言代码的入口点处设置一个断点。(gdb) break start_kernel Breakpoint 1 at 0xffffffff82a3c7e0: file init/main.c, line 928. (gdb) continue Continuing.continue(或c) 命令让被 QEMU 冻结的 CPU 开始执行。内核开始启动并在执行到start_kernel()函数时自动暂停。此时GDB 会显示命中断点并打印出源码上下文Breakpoint 1, start_kernel () at init/main.c:928 928 {现在你可以使用一系列 GDB 命令进行调试list(l): 查看当前断点附近的源码。next(n): 单步执行不进入函数内部。step(s): 单步执行进入函数内部。print variable(p): 打印变量的值。backtrace(bt): 查看函数调用栈。info registers: 查看寄存器状态。例如执行几步next后你可以看到内核初始化的早期流程。5.4 一个完整的调试会话示例假设我们想研究printk函数的内部实现。我们可以在vprintk_func函数printk的核心函数之一设置断点。# 在 GDB 中 (gdb) break vprintk_func Breakpoint 2 at 0xffffffff8110b2c0: file kernel/printk/printk.c, line 2176. (gdb) continue Continuing.然后在 QEMU 的虚拟终端里如果你之前用-nographic启动了带 shell 的系统执行一个会触发printk的命令比如echo “test debug”。GDB 会立刻捕获到这个断点。Breakpoint 2, vprintk_func (fmt0xffffffff82c1fe8c “\002Linux version %s (%s) (%s) %s\\n”, argsargsentry0xffffc9000008fd18) at kernel/printk/printk.c:2176 2176 { (gdb) list 2171 } 2172 EXPORT_SYMBOL(vprintk); 2173 #endif 2174 2175 __printf(1, 0) int vprintk_func(const char *fmt, va_list args) 2176 { 2177 return vprintk_default(fmt, args); 2178 } 2179 2180 /**通过这种方式你可以深入到内核的任何角落进行观察和分析这对于理解内核运行机制或定位疑难 Bug 具有不可估量的价值。6. 进阶开发与调试一个简单的内核模块仅仅运行和调试现有内核还不够我们更需要测试自己编写的代码。下面我们创建一个最简单的内核模块并在 QEMU 环境中进行加载、卸载和调试。6.1 编写一个简单的内核模块在工作目录下创建文件hello_kernel.c// hello_kernel.c #include linux/init.h #include linux/module.h #include linux/kernel.h MODULE_LICENSE(GPL); MODULE_AUTHOR(CSDN Developer); MODULE_DESCRIPTION(A simple hello world kernel module for QEMU debug); static int __init hello_init(void) { printk(KERN_INFO Hello, Kernel World! Module loaded.\n); return 0; // 返回 0 表示初始化成功 } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, Kernel World! Module unloaded.\n); } module_init(hello_init); module_exit(hello_exit);同时创建对应的Makefile# Makefile obj-m hello_kernel.o KERNEL_DIR ? /lib/modules/$(shell uname -r)/build # 注意这里我们需要指向我们**自己编译**的内核源码目录而不是宿主机的 # 假设我们的源码在 ../linux-5.15.148 CUSTOM_KERNEL_DIR ? $(PWD)/../linux-5.15.148 all: # 使用我们编译内核时生成的构建配置和头文件 make -C $(CUSTOM_KERNEL_DIR) M$(PWD) modules clean: make -C $(CUSTOM_KERNEL_DIR) M$(PWD) clean6.2 编译内核模块在包含hello_kernel.c和Makefile的目录下执行make如果一切顺利会生成hello_kernel.ko文件这就是我们的内核模块。6.3 将模块放入根文件系统并测试我们需要让运行在 QEMU 里的内核能够访问到这个.ko文件。将模块复制到 rootfs 目录cp hello_kernel.ko ./rootfs/重新打包 initramfscd rootfs find . -print0 | cpio --null -ov --formatnewc | gzip -9 ../initramfs.cpio.gz cd ..启动 QEMU不带-S正常启动qemu-system-x86_64 \ -kernel ./linux-5.15.148/arch/x86/boot/bzImage \ -initrd ./initramfs.cpio.gz \ -append consolettyS0 nokaslr \ -nographic在 QEMU shell 中操作模块# 进入 QEMU 的 shell 后 $ ls / # 应该能看到 hello_kernel.ko 文件 $ insmod /hello_kernel.ko # 加载模块 $ dmesg | tail -5 # 查看内核日志应看到 “Hello, Kernel World! Module loaded.” $ rmmod hello_kernel # 卸载模块 $ dmesg | tail -5 # 应看到 “Goodbye, Kernel World! Module unloaded.”6.4 调试自定义内核模块如果想调试hello_init函数步骤与调试内核本身类似确保编译模块时开启了调试信息上面的Makefile通过使用自定义内核目录已经隐含了这一点因为我们的内核是带CONFIG_DEBUG_INFO编译的。在 GDB 中你需要加载模块的符号。这需要知道模块被加载到内核地址空间后的基址。在 QEMU shell 中执行cat /sys/module/hello_kernel/sections/.text可以获取模块的.text段地址。在 GDB 中使用add-symbol-file hello_kernel.ko text_addr命令加载符号。然后就可以像之前一样对hello_init等函数设置断点进行调试了。7. 常见问题与排查思路在使用 QEMU 进行内核开发时你可能会遇到以下典型问题。问题现象可能原因排查思路与解决方案QEMU 启动失败报错Could not open ‘xxx.iso’启动参数中指定的镜像文件路径错误或文件不存在。1. 使用绝对路径或检查相对路径是否正确。2. 确保文件已下载完整使用ls -lh确认文件大小正常。内核启动后卡住无输出或报Kernel panic1. 内核配置不支持虚拟硬件。2. 根文件系统 (initrd) 损坏或init程序有问题。3. 内核命令行参数错误。1. 确保使用make x86_64_defconfig这类通用配置。2. 检查initramfs制作流程确保init脚本有执行权限且语法正确。3. 检查-append参数特别是console的设置是否正确。尝试去掉nokaslr看是否与此有关。GDB 连接失败Connection refused1. QEMU 未启动或已退出。2.-s参数未指定或端口被占用。1. 确认 QEMU 进程正在运行 (ps auxGDB 能连接但断点不生效1. 内核地址空间布局随机化 (KASLR) 未禁用。2. 断点设置在错误的地址模块未加载符号。1.必须在内核命令行中加入nokaslr参数。2. 对于内核本身确保加载的是带调试信息的vmlinux文件。对于模块需要先获取加载地址并用add-symbol-file加载符号。模块insmod失败报错Invalid module format模块编译所用的内核版本/配置与当前运行的内核不匹配。确保模块的Makefile中KERNEL_DIR或CUSTOM_KERNEL_DIR指向的是你正在 QEMU 中运行的那个内核的源码目录并且已经make modules_prepare过我们直接make编译内核已包含此步骤。QEMU 占用 CPU 过高QEMU 默认会尽力模拟占用所有可用的宿主 CPU。使用-smp n限制客户机 CPU 核心数如-smp 2。使用-cpu host参数可能利用宿主 CPU 特性提升性能但会降低可移植性。网络无法使用默认的-nographic启动未添加网络设备。添加网络参数例如-netdev user,idnet0 -device e1000,netdevnet0可创建一个用户模式的虚拟网络。更复杂的网络需要桥接或 TAP 设备配置。8. 最佳实践与工程建议将 QEMU 集成到日常内核开发工作流中可以遵循以下建议以提升效率。脚本化一切将冗长的 QEMU 启动命令、GDB 连接命令、根文件系统制作流程都写成 Shell 脚本或 Makefile 目标。例如创建run-qemu.sh,debug-kernel.sh,build-rootfs.sh。# run-qemu.sh 示例 #!/bin/bash KERNEL_IMAGE./linux-5.15.148/arch/x86/boot/bzImage INITRD_IMAGE./initramfs.cpio.gz qemu-system-x86_64 -kernel $KERNEL_IMAGE -initrd $INITRD_IMAGE \ -append consolettyS0 nokaslr quiet -nographic -s -S使用版本控制管理根文件系统将rootfs目录及其构建脚本纳入 Git 管理。可以创建不同的分支或目录来管理包含不同测试工具如perf,strace的文件系统。探索 QEMU 的丰富参数-m 512M: 指定客户机内存为 512MB。-smp 4: 指定客户机为 4 核 CPU。-drive filedisk.img,formatraw: 使用一个持久化的磁盘镜像而不是临时的initramfs。-net nic,modelvirtio -net user,hostfwdtcp::2222-:22: 配置网络并设置端口转发便于从宿主机 SSH 到客户机。结合自动化测试框架对于内核或驱动的大规模测试可以将 QEMU 启动、内核加载、测试用例执行、结果收集的过程自动化集成到 CI/CD 流水线中。性能考量纯软件模拟TCG模式较慢。如果宿主机和客户机架构相同如都是 x86_64可以启用 KVM 加速需要宿主 CPU 和内核支持参数为-enable-kvm这将获得接近物理机的性能。调试复杂问题内核崩溃 (Kernel Panic)QEMU 的-d参数可以输出详细的 CPU 状态、设备日志。结合crash工具分析内核转储。死锁与并发问题QEMU 的确定性执行在某些情况下有助于复现并发 Bug。可以结合内核的lockdep,KCSAN等调试工具。跨架构开发这是 QEMU 的强项。要编译 ARM64 内核你需要安装交叉编译工具链如gcc-aarch64-linux-gnu并使用make ARCHarm64 defconfig配置使用qemu-system-aarch64来启动。制作根文件系统也需要对应架构的busybox。从理解虚拟化与模拟的基本概念到亲手编译内核、制作根文件系统再到使用 GDB 进行源码级单步调试最后完成一个内核模块的完整开发测试循环我们走完了一个 Linux 内核开发者使用 QEMU 的标准工作流。这套环境的价值在于其极致的可控性和可重复性它让你能安全、专注地探索操作系统最核心的领域。当你下次面对一个晦涩的内核 Oops 信息或想验证一个驱动设计时不妨首先启动 QEMU在这个数字沙箱中大胆实验。