ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker+QEMU:构建可复现的Linux内核实验环境

Docker+QEMU:构建可复现的Linux内核实验环境 简介基于Docker与QEMU构建的Linux内核实验环境面向操作系统学习者、内核开发者和测试人员解决了本地搭建内核调试环境繁琐、平台依赖强的问题。资源包共368个文件压缩后约2.53MB以shell脚本、Makefile、Markdown文档、patch补丁和config配置为主同时包含少量C/Python源码与辅助文件目录结构清晰便于一键编译、启动和验证不同版本内核。项目内置多款免安装Linux Lab系统盘方案配合社区快速体验镜像可大幅降低上手门槛涉及v2.6.10、v2.6.11.12等内核版本的构建文件与测试驱动通过QEMU虚拟化可无硬件依赖地完成内核启动、模块加载与驱动调试适合对照源码阅读、实验复现和排错。目前已有130人浏览/学习适合希望快速建立内核实验环境、开展课程设计或科研验证的读者下载使用也可作为自动化内核测试的参考基线。1. 一个 Docker 容器里跑一台内核实验机这套环境解决什么问题很多朋友想学 Linux 内核卡住的地方往往不是源码而是环境依赖缺一个报一个想在 start_kernel 上下断点又怕搞坏系统写个会 panic 的模块更是胆战心惊。Docker QEMU 把这个问题拆成两层——Docker 提供可复制的编译工具链QEMU 模拟一台独立内核实验机炸了不影响宿主容器删掉重建三分钟又是一套干净环境。这套环境适合啃源码的学生、写驱动和内核模块的开发者、要在 CI 里跑内核测试的人。它不需要 KVM 加速也能转起来TCG 模式下 boot 到 shell 只要十几秒有 KVM 透传则更快。文章从 Dockerfile 写到 gdb 断点照着做就能跑通。2. 为什么用容器承载 QEMU而不是直接装虚拟机选型这层很多人纠结我的答案很直接内核实验最贵的成本是“重来”任何能把环境固化成文本的方案都比“装一遍算一遍”可靠。Docker 的 Dockerfile 就是环境清单git 里提交一份任何机器 build 出来都一样。2.1 三种承载方式的取舍性能不是第一优先级宿主机直装 QEMU 最轻但 qemu 版本、gcc、内核头文件全混在系统里半年后升级一次系统环境悄悄变了。整机虚拟机VMware/VirtualBox快照能回滚但镜像动辄几十 GB想分发给同事就得传大文件。Docker 容器跑 QEMU 是折中工具链、QEMU 本身、内核源码都在容器里容器删掉重建就是后悔药学习阶段怎么折腾都不心疼。承载方式隔离性环境可复制分发成本加速条件宿主机直装 QEMU弱依赖混装只能靠脚本低直接读 /dev/kvm整机虚拟机中快照可回滚分发重高几十 GB 级嵌套虚拟化Docker 容器跑 QEMU强Dockerfile 即清单低几百 MB 级可透传 /dev/kvm没有则 TCG这里不是说宿主机方案不能用而是团队协作和学习场景里环境重复性比性能重要。QEMU 在 TCG 模式下模拟一台 x86 机器跑内核启动到串口 shell 约十几秒对实验来说完全够用真到了需要高速执行连续基准测试那一步再加-enable-kvm也不迟。用容器承载 QEMU 还有个隐形好处QEMU 进程崩溃了不影响宿主guest 内核把磁盘写花了也不影响宿主这对内核调试场景是刚需。2.2 容器放行 /dev/kvm 与 /dev/net/tunQEMU 才不拖后腿docker run 时有三个能力位值得注意/dev/kvm、/dev/net/tun、NET_ADMIN capability。它们分别对应 KVM 加速、tap 网络设备创建、网络配置权限。我一般先跑一个最小启动命令docker run -it --rm \ --name kexp \ --device /dev/kvm \ -v /path/to/linux-src:/work/linux \ kexp:latest \ bash--rm保证容器退出即删适合实验舱--device /dev/kvm把宿主机 KVM 设备透传进去QEMU 能用硬件虚拟化加速。注意在 Linux 宿主机且 BIOS 开启虚拟化时才有这个设备Docker Desktop for Mac/Windows 的容器里通常没有QEMU 会自动退回纯软件模拟慢但能跑。-v挂载源码目录避免把内核源码复制进镜像镜像体积保持几百 MB。/dev/net/tun和 NET_ADMIN 只有当你需要 QEMU 的 tap 网络时才加。学习阶段完全可以用 user 模式网络QEMU 内置的 SLIRP 协议栈就能让 guest 上网不需要容器放行任何网络设备。另外提醒一句guest 内核里 insmod 加载模块和容器里 modprobe 是两回事前者操作的是 QEMU 模拟出来的另一个内核容器不需要额外的特权。提示镜像里不装内核源码源码走-v挂载换内核版本、切分支都不用重建镜像。2.3 一个开箱即用的 Dockerfile实验舱镜像我把 Dockerfile 放在项目根目录内容不长但每个包都是踩过坑换来的FROM ubuntu:22.04 RUN apt-get update apt-get install -y \ build-essential \ flex bison libssl-dev libncurses-dev \ libelf-dev bc cpio \ qemu-system-x86 \ gdb kmod \ rm -rf /var/lib/apt/lists/* WORKDIR /work CMD [bash]包名对后内核编译的几个需求build-essential 提供 gcc 和 makeflex/bison 是内核 Kconfig 解析器依赖缺了会在 menuconfig 阶段报错libssl-dev 老版本内核需要新版签名也要libncurses-dev 是 menuconfig 的界面库bc 被某些架构脚本用到cpio 用来打包 initramfsqemu-system-x86 提供 qemu-system-x86_64 二进制。构建命令docker build -t kexp:latest . docker run -it --rm kexp:latest bash如果你的网络环境拉 Ubuntu 基础镜像慢换 registry mirror 是另一个话题。这个镜像里不装内核源码源码通过-v挂载进来这样换内核版本、换分支都不用重建镜像。gdb 和 kmod 是给后面调试模块和查看内核日志用的现在装好省得后面再 build 一层。3. 把 .config 裁剪好再编译增量编译与 bzImage/vmlinux 两个产物环境就绪接下来是编译内核。编译本身不复杂难在配置哪些选项不开内核根本起不来哪些选项开了调试时才能看到足够信息。我的习惯是 defconfig 打底再按实验需求补开关不从头手写 .config。3.1 从 x86_64_defconfig 打底而不是从零手打 .config内核配置文件有数千项手写不现实。x86_64_defconfig 已经覆盖了大部分常见硬件和文件系统我们只需在它基础上补串口和调试选项cd /work/linux make x86_64_defconfig make menuconfigmake x86_64_defconfig生成一份可用的 .configmake menuconfig打开交互界面。需要精确跳转某个选项时按/输入关键字搜索例如搜SERIAL_8250_CONSOLE会列出依赖和当前值回车直接跳到对应位置。如果你习惯脚本化操作可以改用scripts/config系列命令scripts/config --enable DEBUG_INFO \ --enable GDB_SCRIPTS \ --enable IKCONFIG_PROC \ --enable IKCONFIGscripts/config 的好处是能写进脚本、批量改完直接make olddefconfig更新依赖。menuconfig 则适合边查边改。两条路殊途同归最终都落在 .config 上。注意 scripts/config 只改文本不处理依赖关系改完必须跑一次make olddefconfig让 Kbuild 帮你把依赖补全不然可能出现配置项被静默关掉的怪问题。3.2 内核串口与调试必须开的几个配置项不同实验要开的选项不同但以下这些属于“起步配置”第一次搭这套环境最好全开配置项作用CONFIG_DEBUG_INFO生成 DWARF 调试符号gdb 打断点靠它CONFIG_GDB_SCRIPTS内核自带 gdb 辅助脚本提供 lx-symbols 等命令CONFIG_IKCONFIG_PROC把 .config 编进内核guest 里/proc/config.gz可以查当前配置CONFIG_DEVTMPFS_MOUNTboot 时自动挂 devtmpfs/dev 节点不会缺CONFIG_SERIAL_8250_CONSOLEx86 串口 console 驱动QEMU 的 -nographic 输出靠它CONFIG_VIRTIO_PCIQEMU 默认总线模型virtio 设备都依赖它CONFIG_VIRTIO_BLKvirtio 块设备驱动用 -hda/-drive 时必需menuconfig 里逐项搜索并打开DEBUG_INFO 会让 vmlinux 膨胀到几百 MB这不是异常bzImage 不受影响。IKCONFIG_PROC 开启后guest 里zcat /proc/config.gz | grep CONFIG_XXX可以核对运行中的内核配置排错时非常有用——我们经常遇到“以为自己开了其实被依赖关系关掉”的情况这个文件就是黑匣子的钥匙。3.3 make 命令与两个产物bzImage 能启动vmlinux 能调试配置保存后开始编译。强烈建议用 O 独立输出目录make -j$(nproc) Obuild ls -lh build/arch/x86/boot/bzImage build/vmlinux-j$(nproc)按容器可见的 CPU 核心数并行Obuild让所有 .o、.cmd 文件落在 build/ 目录源码树保持干净切分支和重编译都不受影响。build 目录里两个产物要分清arch/x86/boot/bzImage是压缩过的、QEMU -kernel 要用的启动镜像vmlinux是带调试符号的原始 ELFgdb 连上来要加载的是它。之后每次改动代码只需重复make -j$(nproc) Obuild内核会按时间戳做增量编译。比如改一个 driver 目录下的文件通常几十秒内就能重新出 bzImage。想用 clang 的话可以加LLVM1整体切到 LLVM 工具链但第一次不要折腾gcc 默认路线最省心。还有一个经验编译时如果报 “No rule to make target”多半是你改了 .config 后没跑make olddefconfig跑一遍再 make 就好。4. 制作最小 rootfs把 Linux 内核跑在 QEMU 里bzImage 是“带电的内核”但它还需要一个用户态环境才能跑起来。最小方案是用 busybox 做一个 initramfs一个静态编译的 shell 加上几个核心工具打包成 cpio 扔给内核。4.1 用 busybox 静态编译做 initramfs在容器内下载 busybox 源码并静态编译cd /work wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar xf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make defconfig make menuconfigmenuconfig 里进入Settings找到Build static binary (no shared libs)勾上。静态编译后 busybox 不依赖动态库放进 initramfs 才能直接执行。然后编译并安装make -j$(nproc) make installmake install会把 busybox 连同符号链接装进_install/目录。接下来在这个目录里补 init 脚本和设备节点打包成 initramfscd _install mkdir -p proc sys dev mknod dev/console c 5 1 mknod dev/null c 1 3 cat init EOF #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys echo Welcome to kernel lab exec /bin/sh EOF chmod x init find . | cpio -o -H newc | gzip /work/initramfs.imginit 是内核在挂上 rootfs 后启动的第一个进程PID 1这里挂上 proc/sysfs然后 exec 一个 shell。mknod 的 console 和 null 这两个设备节点在 CONFIG_DEVTMPFS_MOUNT 开启时会自动出现我们手动建一份是双保险防止配置没生效时 init 连 console 都打不开。find . | cpio -o -H newc | gzip是打包 initramfs 的标准姿势产物只有几百 KB。4.2 QEMU 启动参数逐个拆解一切就绪启动qemu-system-x86_64 \ -kernel /work/linux/build/arch/x86/boot/bzImage \ -initrd /work/initramfs.img \ -append consolettyS0 panic-1 oopspanic \ -m 512M \ -smp 2 \ -nographic \ -no-reboot参数逐个说-kernel指定 bzImage 路径-initrd指向刚打的 initramfs.img-append是传给内核的 cmdlineconsolettyS0让内核日志走串口而不是 VGA这是 -nographic 能输出的前提。panic-1表示内核 panic 后不尝试自动重启留在现场方便看打印oopspanic把 oops 也升级成 panic——调试时宁可停死也不要带病运行。-m 512M给 512MB 内存-smp 2两个核-nographic把串口接到当前终端-no-reboot禁止 QEMU 在 guest 重启时自动重开保留现场。4.3 从串口 shell 到主动退出验证环境闭环正常启动后终端会从 “Booting Linux” 一路滚到Welcome to kernel lab和 shell 提示符。在 shell 里验证环境uname -a cat /proc/cmdline ls /sys/class/netuname -a确认内核版本和编译时间/proc/cmdline能回看我们传的参数/sys/class/net能看到 lo 和 QEMU user 网络给的网卡。测试完退出 QEMU 用Ctrl-A然后按x这是 -nographic 模式下的固定退出方式比直接 kill 干净。如果想模拟 arm64命令要换QEMU 用qemu-system-aarch64 -M virt -cpu max内核用交叉编译产物工具链在 Ubuntu 里装crossbuild-essential-arm64。console 参数也变成ttyAMA0不能直接照抄 x86 的命令。我的建议是先把 x86_64 跑通再考虑 aarch64不然 QEMU 参数、工具链、设备树三块一起排查会很吃力。提示如果你加了 QEMU 图形界面参数去掉 -nographic内核日志默认走 VGA串口输出反而可能为空。用 -nographic 配合 consolettyS0是最省心的一对组合。5. QEMU 内核实验避坑清单五个高频翻车点这套环境我搭过很多次换机器、换系统、换 Docker Desktop 版本都会踩到重复的坑。下面五条按“现象 → 原因 → 解决”记录基本都是第一次就能碰上的。5.1 串口黑屏QEMU 起来了终端却没有任何日志现象qemu-system-x86_64 跑起来终端一片黑按键盘也没反应。原因cmdline 缺consolettyS0或者内核没开 CONFIG_SERIAL_8250_CONSOLE。-nographic 只是把串口接到终端内核日志如果走默认 VGA 就不会出现在这里。解决先在 -append 里确认有consolettyS0再回 menuconfig 检查 8250 console 选项。区分方法按Ctrl-A再按c进 QEMU monitor如果 monitor 能响应而系统没输出问题基本就是内核 console 配置。console 参数本身顺序也有讲究多个 console 时内核按最后一个生效别把 ttyS0 写在 tty0 前面还不自觉。5.2 启动慢得离谱Docker Desktop 里没有 /dev/kvm现象同样的命令在 Linux 机器上秒级进 shell换到 Docker Desktop 容器里要几分钟。原因macOS 和 Windows 的 Docker Desktop 底层是虚拟机容器内访问不到 /dev/kvmQEMU 自动退回 TCG 纯软件模拟执行速度差一个量级。解决如果只用这套环境做实验把-smp 1 -m 256M降下来TCG 下等待时间可以接受如果需要速度必须在能访问 /dev/kvm 的 Linux 宿主机上跑并加--device /dev/kvm。给 QEMU 显式加-enable-kvm在没有 KVM 的机器上会直接报Could not access KVM kernel module这不是环境坏了去掉这个参数即可。想确认当前到底用没用 KVM看 QEMU 启动日志里有没有类似Using KVM for x86-64的提示没有就是退化到 TCG 了。5.3 创建 tap 网络报 Operation not permitted现象想给 QEMU 用 tap 网络启动时报Could not configure /dev/net/tun: Operation not permitted。原因容器没有挂载 /dev/net/tun也没有 NET_ADMIN capability而 tap 设备创建正好需要这两个权限。解决docker run 时加--device /dev/net/tun --cap-add NET_ADMIN。如果只是让 guest 能上网用 QEMU 的 user 模式网络就够qemu-system-x86_64 ... -netdev user,idn0 -device virtio-net-pci,netdevn0user 模式走 QEMU 内置的 SLIRP 协议栈guest 可以获得网络访问能力容器不需要任何网络特权。等真要做网桥、多机互通时再上 tap 也不迟。5.4 GDB 的 target remote 连不上 QEMU现象QEMU 加了 -sgdb 里target remote :1234一直 Connection refused。原因-s 等价于-gdb tcp::1234监听的是 QEMU 进程所在网络空间。QEMU 跑在容器里时默认只监听容器内 127.0.0.1宿主机上的 gdb 当然连不进来。解决QEMU 启动参数改成-gdb tcp:0.0.0.0:1234并且 docker run 时映射端口-p 1234:1234。如果你用 VS Code 调内核launch.json 里配置 attach 到127.0.0.1:1234原理和命令行 gdb 完全一样——VS Code 只是把 gdb 的 target remote 包装成了图形界面。5.5 VFS: Unable to mount root fs挂不上根文件系统现象用-hda rootfs.ext4启动内核走到最后报Kernel panic - not syncing: VFS: Unable to mount root fs。原因cmdline 写的 root 设备名和 QEMU 实际挂载的磁盘接口对不上。用-hda默认走 IDE 接口设备是 /dev/sda用-drive ifvirtio或显式-device virtio-blk时设备变成 /dev/vda。内核缺对应驱动也会报同样错误。解决先确认自己用哪种接口-hda配root/dev/sda rwvirtio 配root/dev/vda rw同时确保内核开了 CONFIG_ATA 或 CONFIG_VIRTIO_BLK。这也是我一开始就推荐 initramfs 的原因cpio 里的 shell 不依赖任何磁盘驱动先把内核跑起来再扩展存储省掉一半排错面。以上五条覆盖了我在新机器搭这套环境时遇到的大部分问题。剩下的一些怪问题多半是内核 Kconfig 依赖没理清回去 menuconfig 里看选项的依赖说明能解决大半。6. 用 GDB 断点内核、用 virtfs 共享文件从启动到可调试6.1 用 gdbstub 在 start_kernel 下断点把 QEMU 退出来重新加两个参数启动qemu-system-x86_64 \ -kernel /work/linux/build/arch/x86/boot/bzImage \ -initrd /work/initramfs.img \ -append consolettyS0 nokaslr \ -m 512M -smp 1 \ -nographic \ -s -S-s 开启 gdbstub 监听 1234 端口-S 让 CPU 在启动前挂起等调试器接入。然后另开终端gdb /work/linux/build/vmlinux (gdb) target remote :1234 (gdb) b start_kernel (gdb) ctarget remote :1234连上 QEMU断点在 start_kernel这是架构无关的内核入口函数比断在汇编阶段直观。nokaslr 关闭内核地址随机化让符号地址和运行时地址一一对应。连上后lx-symbols可以重新加载内核模块符号需要 CONFIG_GDB_SCRIPTS后续 insmod 的模块也能在 gdb 里看到。6.2 virtfs 共享目录宿主机改代码guest 里拉取QEMU 再加一个参数可以把宿主机目录直接共享给 guest-virtfs local,path/work/share,mount_taghost0,security_modelnoneguest 里挂载mount -t 9p -o transvirtio host0 /mnt内核需要 CONFIG_NET_9P_VIRTIO 和 CONFIG_9P_FS。这样编译好的 .ko 放进 /work/shareguest 里insmod /mnt/xxx.ko就能加载不用每次重新打 initramfs。我现在的日常工作流就是一条线改代码 →make Obuild -j→ 把模块 cp 到共享目录 → guest 里 insmod → dmesg 看输出。要断点时再拉起 gdb。这套组合拳用熟了内核实验从“怕弄坏电脑”变成“随便折腾”。我自己的习惯是把 Dockerfile 和内核配置脚本一起提交进 git换机器先 build 镜像、挂载源码目录十分钟内拉起全套环境。TCG 模式下偶尔慢一点但慢正好逼着我把内核配置和启动参数看清楚。希望这套方案能帮到你。本文还有配套的精品资源点击获取
返回列表