资讯动态

Docker+QEMU 搭建 Linux 内核调试实验环境:编译驱动不再折腾

发布时间:2026/10/19 17:44:00 来源:尧图企业网站定制
简介基于 Docker 与 QEMU 的 Linux 内核实验环境定位为一套可快速启动、可复用的内核学习与开发底座面向正在学习内核源码、准备进行驱动开发或需要搭建 CI 调试环境的开发者。项目参考社区 Linux Lab 的组织方式将容器化调度与模拟器启动结合起来省去手动配置交叉编译和根文件系统的繁琐步骤。压缩包共 368 个文件大小约 2.53MB其中 84 个 shell 脚本承担环境初始化、磁盘挂载与启动控制59 个 Makefile 管理多版本内核的下载、配置和编译52 个 Markdown 文档提供使用指南和实验手册另有补丁、config 配置、C/汇编源码、Python 脚本等覆盖了从构建到调试的完整链路。当前已有 131 人学习下载。借助这份资源可以快速获得一套内核构建与 QEMU 启动模板、gdb 调试辅助文件、典型字符设备驱动示例以及配套的排错配置便于按需修改和扩展自己的内核实验场景。1. 从 Docker 拉起一套 Linux 内核实验环境别再让编译吃掉整个下午某开发者在模拟项目X里调一个 2.6.x 内核的字符设备驱动改一行 printk 要等半小时重新编译改到第三次直接把宿主机的交叉编译链弄崩了前后折腾两天。后来换到这套基于 Docker QEMU 的 Linux 内核实验环境同一份驱动代码从编译到跑起来压缩到几分钟还能同时维护 v2.6.10 和 v2.6.11/12 两套内核树互不干扰。这就是这套操作系统实验环境的典型价值内核学习、内核开发、内核测试都在容器里完成宿主机的环境再乱也不影响实验进度。适合三类人正在写内核模块或字符设备驱动的开发者、做操作系统课程实验的学生、需要反复调试崩溃转储和内核符号的运维工程师。装不好它你会反复撞上依赖冲突、多版本内核互相覆盖、调试符号对不上的问题今天这篇文章把这些坑逐个踩平。2. 环境准备与版本选型Docker 镜像、QEMU 前端、内核源码树2.1 为什么把内核实验环境塞进 Docker裸机装 QEMU 跑内核不是不行但你要么得在宿主机装一堆编译工具链要么得忍受多套内核源码互相污染。Docker 把整个工具链、内核源码、磁盘镜像全部隔离在一个容器里宿主机只需要装一个 Docker 引擎剩下的脏活都在容器内解决。这套环境的核心思路很直接QEMU 负责模拟硬件Docker 负责提供编译和运行所需的用户态工具。内核编译产物vmlinux、模块 .ko 文件放在容器内的挂载卷里QEMU 启动时直接读取这些产物通过 -kernel 参数引导内核不再需要单独制作引导盘。容器内带了完整的 build-essential、gdb、交叉编译链和 QEMU 前端省掉最折磨人的依赖安装阶段。我一般会专门用一个数据卷保存源码和编译产物这样容器删了重建编译缓存和内核树还在升级环境不会连坐清空劳动成果。2.2 软硬件最低要求与版本对照先确认环境指标别到最后一步才发现资源不够。下面是个人实际跑过的参考配置不是官方最低要求但按这个准备会比较省心项目推荐配置说明宿主机内存8 GB 起步16 GB 更稳QEMU 里编译内核峰值吃 23 GB容器再占一部分磁盘空间30 GB 可用内核源码 编译产物 磁盘镜像三套内核树会到 15 GB 以上Docker 版本20.10 以上低版本对 /dev/kvm 透传支持不好QEMU 版本7.0 以上容器内老版本对 gdbstub 的支持不完整CPU 虚拟化必须支持并开启见 5.3 节没打开会慢到怀疑人生Docker 安装完成后先验证当前用户能否直接用 docker 命令。用id查看用户组如果没有 docker 组权限sudo usermod -aG docker 当前用户后重新登录。很多人第一步就卡在这里docker ps 都要加 sudo后面每一条命令都会绕。2.3 拉取基础镜像与目录规划环境里用到的容器镜像没有特殊定制基于常规 Ubuntu/Debian 基础镜像就能自己搭但社区已经把这些步骤固化成镜像拉下来就能用。这里演示从零构建的目录规划方式方便理解容器内的文件是怎么组织的。# 创建工作目录与数据卷 mkdir -p ~/linux-lab/{kernels,images,scripts,drivers} docker volume create linux-lab-build # 启动一个带 KVM 透传的交互容器 docker run -it --name linux-lab \ --device /dev/kvm \ -v ~/linux-lab:/work \ -v linux-lab-build:/build \ ubuntu:22.04 /bin/bash--device /dev/kvm用于把宿主机的 KVM 加速能力透传给容器没有这行参数 QEMU 会退化成纯软件模拟启动一个 2.6.x 内核要几十秒且加载驱动后响应明显迟钝。-v ~/linux-lab:/work是宿主机和容器共享的工作目录内核源码、驱动代码放这里宿主机用编辑器改容器内直接编译。-v linux-lab-build:/build是匿名的编译缓存卷专门放中间产物。进入容器后第一件事是装工具链apt-get update apt-get install -y build-essential gdb qemu-system-x86 git。装完后确认 qemu-system-x86_64 可用这是后面所有步骤的前置条件。2.4 用 Makefile 管理多版本内核v2.6.10 与 v2.6.11/12 共存这套环境最值得抄作业的地方在于多版本内核管理。环境里同时存在Makefile.linux_v2.6.10和Makefile.linux_v2.6.11.12两套 Makefile 入口分别对应两个历史版本的内核树。手动维护两套源码树很容易发生目录混用Makefile 里通过变量隔离了两个版本的构建目录。# Makefile.linux_v2.6.10 核心片段 KERNEL_DIR : /build/linux-2.6.10 BUILD_DIR : /build/linux-2.6.10-build KERNEL_IMG : $(BUILD_DIR)/arch/x86/boot/bzImage linux-v2.6.10: mkdir -p $(BUILD_DIR) $(MAKE) -C $(KERNEL_DIR) O$(BUILD_DIR) defconfig $(MAKE) -C $(KERNEL_DIR) O$(BUILD_DIR) -j$(nproc)另一份Makefile.linux_v2.6.11.12结构相同只是 KERNEL_DIR 指向 2.6.11/2.6.12 的合并树。两套内核的 vmlinux 符号文件放在各自的 BUILD_DIR 下gdb 加载调试符号时不会串。这里有个值得注意的设计两个版本共用同一个 QEMU 启动脚本但通过环境变量切换内核镜像和磁盘镜像路径。这样老内核的特性比如早期 misc 设备的注册方式和新内核的行为可以在同一套脚本里对比测试这正是做操作系统实验时最需要的功能。3. 构建与启动实验环境从编译配置到 QEMU 引导3.1 初始化工作树与内核源码获取进入容器以后第一个坑往往是源码从哪来。这套环境的backup目录里放着预先验证过的内核源码压缩包同时根目录下有 AUTHORS 记录了环境维护者的联系方式。优先用 backup 里的源码包避免去外网拉取时遇到版本不一致的问题。# 在容器 /work 目录下操作 cd /work tar -xjf backup/linux-2.6.10.tar.bz2 -C /build/ tar -xjf backup/linux-2.6.11.12.tar.bz2 -C /build/ ls /build/linux-2.6.10/init/main.c # 验证源码是否完整解压后先看一眼init/main.c是否存在这是内核入口文件缺失说明压缩包损坏。2.6.x 内核源码体积不大全部解压完约 400 MB比 5.x 内核动辄 1 GB 以上友好很多。之后编译时用O指定独立构建目录保持源码目录干净这是老内核构建最重要的习惯。注意不要用 root 直接在源码目录里编译 2.6.10这个版本对较新的 GCC 有兼容性问题。更稳妥的做法是显式安装较老的交叉编译器比如 gcc-7 或 gcc-8配合使用。如果编译中途报include/linux/compiler-gcc.h相关错误基本就是 GCC 版本太新了。3.2 编译配置与 gdbinit.auto 的联动正常编译内核需要先跑make menuconfig但这套环境里 Makefile 已经预设了默认配置直接跑目标即可。环境根目录下的gdbinit.auto是一个自动加载的 gdb 配置文件它的作用是在启动 gdb 时自动完成三件事加载 vmlinux 符号表、连接到 QEMU 的 gdbstub 端口、设置默认断点。# 编译 2.6.10 内核 make -f Makefile.linux_v2.6.10 linux-v2.6.10 # 编译 2.6.11.12 内核 make -f Makefile.linux_v2.6.11.12 linux-v2.6.11.12执行后先编译 2.6.10大约几分钟取决于 CPU 核数。Makefile 里 -j$(nproc) 自动用满所有核老内核编译并行度太高偶尔会出依赖问题如果报错就把 nproc 换成固定数值 4 重新执行。编译完成后重点看gdbinit.auto的内容它会告诉 gdb 从哪个路径加载符号# gdbinit.auto 关键内容 file /build/linux-2.6.10-build/vmlinux target remote :1234 break start_kernel continuefile命令加载 vmlinux 符号文件这决定了断点能否命中源码行号target remote :1234连接 QEMU 的调试端口break start_kernel把断点下在系统启动最早的 C 函数入口。这里有个细节连接端口要在 QEMU 启动时通过-s参数打开端口固定为 1234不能改两边必须对齐。3.3 启动 QEMU 实例把编译产物跑起来内核编译好了驱动模块还在 /build 目录里现在要把整个系统引导起来。QEMU 启动参数里最关键的是-kernel、-append和-nographic三件套。cd /build/linux-2.6.10-build qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -initrd /work/images/initrd.img \ -append root/dev/ram0 consolettyS0 rdinit/sbin/init \ -nographic \ -m 512 \ -smp 2 \ -s -S-kernel指定 bzImage 内核镜像-initrd使用预先制作的 initrd 文件系统里面有基本的 busybox 和驱动加载工具-append传给内核的启动参数里consolettyS0让内核日志输出到串口配合-nographic才能在当前终端里看到启动日志-s -S组合是调试模式-s打开 1234 端口 gdbstub-S让 CPU 在启动时冻结等 gdb 连接后再开始执行。如果不需要调试把-s -S去掉即可直接启动进入系统。启动后看到内核日志滚动直到出现init started之类的提示说明引导成功。如果卡住不动多数是 initrd 里的 /sbin/init 路径不对用rdinit/bin/sh代替看能否进入 shell 来排查。4. 驱动模块示例与调试技巧从源码到 insmod 验证4.1 misc_loop_drv.c 与 ldt.c字符设备的两种写法环境里带着几个驱动示例misc_loop_drv.c是 misc 设备驱动演示了注册一个 miscdevice 结构体并实现 read/write 回调ldt.c则用经典register_chrdev方式注册字符设备。两个文件对比着看能直观感受老内核里两种字符设备注册接口的差异。/* misc_loop_drv.c 关键结构 */ #include linux/miscdevice.h #include linux/fs.h static ssize_t loop_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { char kernel_buf[] linux-lab-loop\n; return simple_read_from_buffer(buf, count, ppos, kernel_buf, strlen(kernel_buf)); } static struct miscdevice loop_dev { .minor MISC_DYNAMIC_MINOR, .name linux_lab_loop, .fops loop_fops, }; static int __init loop_init(void) { return misc_register(loop_dev); }MISC_DYNAMIC_MINOR让内核自动分配次设备号省去手动选号misc_register在 2.6.x 里比register_chrdev轻量不需要预先分配设备号区间适合教学演示。这段驱动编译成 .ko 后insmod 会在 /dev 下自动生成linux_lab_loop节点直接用 cat 就能测试 read 回调。# 编译驱动模块 cd /work/drivers make -C /build/linux-2.6.10-build M$(pwd) modules # 进入 QEMU 系统后加载 insmod misc_loop_drv.ko cat /dev/linux_lab_loop-C /build/linux-2.6.10-build指定内核构建目录M$(pwd)告诉 kbuild 去当前目录找模块源码。insmod 后 cat 设备文件能输出linux-lab-loop说明读写回调正常执行。如果 cat 报 No such device先 dmesg 看内核日志有没有注册失败的信息。4.2 dio.c 与 ldt_plat_drv.c块设备 IO 与平台驱动dio.c演示直接 IODirect IO的读写路径ldt_plat_drv.c则是平台总线驱动的例子它把一个平台驱动挂到 QEMU 模拟的 virtio 平台上。这两份代码对应内核实验中更高阶的部分块设备层和驱动模型。# 编译并加载平台驱动 cd /work/drivers make -C /build/linux-2.6.11.12-build M$(pwd) modules # 在 QEMU shell 内部 insmod dio.ko insmod ldt_plat_drv.ko ls -l /sys/bus/platform/drivers/ldt_plat_drv/平台驱动加载成功后/sys/bus/platform/drivers/ldt_plat_drv/目录下会出现设备节点说明 probe 函数被正确调用。调试这类驱动建议在ldt_plat_drv_probe函数里下断点gdb 里break ldt_plat_drv_probe然后在 QEMU 里 insmod看断点是否命中。如果不命中先确认驱动里的compatible字符串和设备树里的是否完全一致平台驱动最常见的问题就是名字匹配不上。dio.c 涉及的直接 IO 路径在 2.6.x 和后续版本差异较大同一份代码在不同版本内核树里编译结果可能不同这恰好是这套环境多版本并存的用武之地。5. 常见问题排查启动失败、调试断点失效、环境残留5.1 config.aarch64.virt.broken 为什么带着 broken 后缀环境里有个文件名config.aarch64.virt.broken第一次看到容易误认为是跨架构的 AArch64 配置文件。实际上这个文件是几次构建失败留下的半成品配置维护者特意把 .broken 后缀保留下来作为警示。直接拿来编译会报错因为它内部引用了不存在的驱动配置项。现象按常规方法把 config 改名后编译中途报arch/arm64/configs相关路径缺失。原因这个配置对应的 AArch64 虚拟化平台在 2.6.10 时代的 QEMU 模拟中不完整config 里开启的某些设备在virt机型上根本没有实现。解决以backup目录里对应的干净配置为准不要使用任何带 broken/failed 后缀的 config 文件。如果必须用 AArch64 目标单独在容器里安装qemu-system-aarch64和对应交叉编译链并重新生成配置。这个坑的教训是源码仓库里的 backup 目录和异常配置文件是前人踩坑留下的记录pristine 状态的文件才是能用的。5.2 gdb 报错 Remote g packet reply is too long现象按 gdbinit.auto 连接 QEMU 的 1234 端口后gdb 报Remote g packet reply is too long随后断点无法命中。原因QEMU 启动时用的 CPU 型号和内核编译时的目标架构不完全匹配。早期 QEMU 默认 CPU 特性集和 gdb 期望的寄存器布局不一致导致 gdb 解析寄存器的包长度出错。解决在 gdbinit.auto 里追加两行配置或者启动 QEMU 时显式指定 CPU 型号。我一般用如下方式# gdbinit.auto 追加 set architecture i386:x86-64 set disassembly-flavor intel # QEMU 启动参数追加 -cpu qemu-system-x86_64 ... -cpu qemu64set architecture强制 gdb 使用指定架构描述-cpu qemu64让 QEMU 暴露一个稳定的 CPU 特性集两边匹配后 packet 长度就正常了。这个报错非常经典几乎每个用 QEMU gdb 调试内核的人都会遇到一次关键是意识到它不是 gdb 的问题而是 CPU 特性协商的问题。5.3 Docker 里 /dev/kvm 缺失或权限不足现象QEMU 启动参数带-enable-kvm时报Could not access KVM kernel module或者-accel kvm提示权限被拒。原因容器里默认看不到宿主机的 /dev/kvm 节点Docker 启动参数里没透传设备另外有些云主机的 CPU 虚拟化默认关闭。解决先确认宿主机能否访问 /dev/kvm再启动容器时追加设备映射。# 宿主机检查 ls -l /dev/kvm # 容器启动追加 docker run -it --device /dev/kvm \ --group-add $(stat -c %g /dev/kvm) \ linux-lab--group-add $(stat -c %g /dev/kvm)让容器进程加入与 /dev/kvm 相同的用户组避免容器内用户无权限打开该设备。如果宿主机本身没有 /dev/kvm是云主机没开启嵌套虚拟化只能放弃 KVM 加速退用 TCG 软件模拟。2.6.x 内核对性能要求不高TCG 模式也能跑但 gdb 单步调试会明显变慢。5.4 文件系统镜像写坏导致启动卡在 clean 检查现象QEMU 启动后内核日志停在checking for the dirty flag随后系统长时间卡住或者在 ext4 日志回放阶段反复崩溃。原因上次实验时 QEMU 异常退出磁盘镜像没有正常卸载文件系统留下脏标记老内核在启动时做完整性检查超时。解决用 QEMU 的 snapshot 模式启动所有写入落在临时快照里不污染原始镜像。qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -initrd /work/images/initrd.img \ -drive file/work/images/rootfs.img,formatraw,snapshoton \ -append root/dev/sda consolettyS0 \ -nographicsnapshoton是关键参数它让镜像的所有写操作重定向到内存快照QEMU 退出后原镜像保持干净。代价是系统内所有改动不会持久化适合编译驱动、调试内核这类不需要保存现场的实验。如果必须持久化又已经损坏了镜像用 e2fsck 在宿主机上修复注意不要挂载后修复必须在离线状态下执行。5.5 内核日志看不到输出或输出乱码现象QEMU 启动参数里带了-nographic但终端里看不到任何日志或者日志是乱码。原因consolettyS0没传进内核或者串口波特率与 QEMU 模拟的不一致。2.6.x 内核默认控制台是 tty0VGA没指定console参数时所有 printk 都发往显示终端而不是串口。解决确保-append里同时带consolettyS0和earlyprintkserial前者指定启动后的日志控制台后者让启动早期的 panic 信息也输出到串口。乱码问题常见于终端模拟器对串口时序处理不好可以先确认内核日志里是否有console [ttyS0] enabled字样代码里这是register_console的打印。这一条建议每次启动前都过一眼排查起来最省时间。6. 装完怎么验证跑通启动、insmod、打断点的三连验证环境装完不能算了事我每次都会用一组标准动作验证整条链路是否通畅。这套验证方法花了很長時間才稳定下来每一条命令都是为了同时覆盖内核引导、驱动加载、调试通道三个环节缺一个都可能漏掉隐患。# 第一步启动 2.6.10 内核不带 -S 直接进系统 qemu-system-x86_64 \ -kernel /build/linux-2.6.10-build/arch/x86/boot/bzImage \ -initrd /work/images/initrd.img \ -append root/dev/ram0 consolettyS0 rdinit/sbin/init \ -nographic -m 512 # 等待出现 shell 提示符后加载驱动并检查设备节点 insmod misc_loop_drv.ko ls -l /dev/linux_lab_loop cat /dev/linux_lab_loop第一条验证做完的标准是看到内核版本号打印、能进入 shell、加载驱动后 /dev 节点出现且 cat 有输出。这套流程跑通说明编译、引导、模块加载三个环节没问题。# 第二步带调试参数重新启动验证 gdb 断点 qemu-system-x86_64 -kernel ... -s -S gdb -x /work/gdbinit.auto /build/linux-2.6.10-build/vmlinuxgdb 连接后输入continue断点应停在start_kernel处。继续next和step验证单步执行再break sys_init_module后正常引导进系统在 shell 里 insmod 驱动gdb 应再次命中。这一步把用户态加载行为和内核态执行路径全部串起来调试通道才真正算通过。整个验证流程熟练后不超过十分钟。从那以后我每次装完这套环境都强制先走一遍启动、insmod、断点三连确认通过后再开始正经实验省下的排错时间远比测试时间多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑