1. 从应用 write 到内核断点:这条链路到底卡在哪
WSL 里跑 QEMU 调试 Linux 内核,很多人第一次搭完能启动、能进 shell,但一旦想验证「应用调用驱动接口,最后落到内核代码」这条链路,就会卡在几个具体位置:QEMU 启动参数少写了-s -S,GDB 连不上;内核编译时没开CONFIG_DEBUG_INFO,断点打上去是灰的;Buildroot 生成的 rootfs 里没有对应的设备节点,echo写进去直接报 No such device;或者模块编译进内核了,但mknod的主次设备号和启动日志里注册的对不上。
这篇要解决的就是这套「应用 → 驱动接口 → 内核」的调试骨架。适合已经在 WSL 里用 QEMU + BusyBox/Buildroot 跑起最小系统、想进一步验证字符设备驱动读写路径的人。核心思路是:把驱动编译进内核,用 QEMU 的 GDB stub 挂上调试器,在scull_write/scull_read里打断点,从用户态echo/cat一路跟到内核函数。同时把调试过程中用到的模型调用配置统一走 TaoToken 的 Key/API 通道,避免多个工具各配一套 Key 的混乱。
我试过在 WSL2 里直接跑这套流程,踩过的坑主要集中在 QEMU 参数顺序、内核配置项和 rootfs 设备节点这三块。下面按可复制的顺序拆开。
2. TaoToken 前置:统一 Key 与 API 通道
调试内核时经常需要让编辑器里的 AI 辅助、脚本里的模型调用、以及本地 agent 共用一套凭证。如果每个工具单独配 Key,改一次要动好几个地方。TaoToken 的作用是把这些调用收敛到一个入口。
先到官网注册并拿到 Key:
# 官网入口(含来源标识) https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=登录后在控制台创建 API Key:
# 控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite # API Keys 管理页 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite拿到 Key 后,在 WSL 里用环境变量统一管理,避免写死在脚本里:
# ~/.bashrc 或 ~/.zshrc export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"注意:API 地址不带 UTM 参数,直接写
https://taotoken.net/api即可。Key 不要提交到 git,建议放.env并加入.gitignore。
验证 Key 是否可用,用一条最小请求:
curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" | head -c 300如果返回模型列表 JSON,说明通道通了。后续在编辑器插件或脚本里,把 base_url 指向https://taotoken.net/api,Key 用同一个环境变量,就不用重复配置。
需要看接入细节的话,文档页在这里:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite3. 可复制配置:QEMU 启动参数 + Buildroot + 内核调试选项
3.1 QEMU 启动参数骨架
这套参数是整条链路的入口,-s -S是 GDB 调试的关键,-nographic让串口输出直接打到终端:
qemu-system-x86_64 \ -kernel bzImage \ -hda rootfs.ext4 \ -hdb shadisk.img \ -append "root=/dev/sda console=ttyS0" \ -s -S \ -smp 1 \ -nographic参数含义对照:
| 参数 | 作用 | 调试相关 |
|---|---|---|
-kernel bzImage | 指定内核镜像 | 必须是带 debug info 编译的 |
-hda rootfs.ext4 | 根文件系统 | Buildroot 生成 |
-hdb shadisk.img | 附加磁盘 | 可选,用于测试块设备 |
-append "root=/dev/sda console=ttyS0" | 内核命令行 | 串口输出到终端 |
-s | 在 1234 端口开 GDB stub | 必须 |
-S | 启动时暂停,等 GDB 连接 | 必须 |
-smp 1 | 单核 | 避免多核断点混乱 |
-nographic | 无图形,串口直连 | 方便看 printk |
注意:
-s等价于-gdb tcp::1234。如果 1234 被占用,改成-gdb tcp::1235,GDB 里对应target remote :1235。
3.2 Buildroot 配置片段
Buildroot 负责生成 rootfs,关键是打开串口控制台和必要的工具:
# 在 buildroot 目录下 make menuconfig需要确认的选项:
Target options -> Target Architecture = x86_64 System configuration -> Root filesystem overlay directories = (可选,放自定义脚本) -> Run a getty (login prompt) after boot = y -> getty options -> TTY port = ttyS0 -> Baudrate = 115200 Filesystem images -> ext2/3/4 root filesystem = y -> ext4 variant = y Target packages -> Debugging, profiling and benchmark -> gdb = y # 目标机上的 gdb,可选 -> Shell and utilities -> busybox = y生成 rootfs:
make -j$(nproc) # 产物在 output/images/rootfs.ext43.3 内核调试选项
内核必须带 debug info,否则 GDB 断点无效。在make menuconfig里改这三处:
Kernel hacking -> Compile-time checks and compiler options [*] Compile the kernel with debug info [*] Provide GDB scripts for kernel debugging Processor type and features -> [ ] Randomize the address of the kernel image (KASLR)KASLR 必须关掉,否则每次启动内核地址随机化,GDB 里的符号对不上。
编译:
export ARCH=x86 make x86_64_defconfig make menuconfig # 改上面三项 make -j$(nproc)编译完成后复制产物到工作目录:
cp arch/x86_64/boot/bzImage ./ cp vmlinux ./3.4 把驱动编译进内核
以 ldd3 的 scull 为例,把它挂到内核驱动树里。顶层 Kconfig 加一行:
# drivers/Kconfig source "drivers/char/Kconfig" source "drivers/ldd3/Kconfig"顶层 Makefile 加一行:
# drivers/Makefile obj-y += char/ obj-y += ldd3/ldd3 目录下的 Kconfig:
# drivers/ldd3/Kconfig menu "ldd3 devices" source "drivers/ldd3/scull/Kconfig" endmenuscull 的 Kconfig:
# drivers/ldd3/scull/Kconfig menuconfig SCULL tristate "chapter 3 scull devices" default y help LDD scull driver. if SCULL config SCULL_DEBUG tristate "chapter 3 scull devices debug" help LDD scull debug. endifscull 的 Makefile:
# drivers/ldd3/scull/Makefile obj-$(CONFIG_SCULL) += main.o改完执行增量编译:
make -j$(nproc)第一次加模块要完整编译,之后只改main.c就是增量编译,快很多。
4. 验证请求:从 GDB 断点到串口输出
4.1 启动 QEMU 并挂 GDB
先启动 QEMU(带-s -S,会暂停):
qemu-system-x86_64 \ -kernel bzImage \ -hda rootfs.ext4 \ -hdb shadisk.img \ -append "root=/dev/sda console=ttyS0" \ -s -S -smp 1 -nographic另开一个 WSL 终端,启动 GDB:
gdb vmlinux在 GDB 里连接:
(gdb) target remote :1234 (gdb) break scull_init_module (gdb) continueQEMU 那边会继续启动,命中scull_init_module断点后可以单步:
(gdb) next (gdb) print scull_major (gdb) print scull_nr_devs4.2 在 scull_write 打断点
继续运行到系统启动完成,登录 root。然后在 GDB 里:
(gdb) break scull_write (gdb) continue回到 QEMU 串口,创建设备节点并写入:
mknod /dev/scull0 c 249 0 ls -l /dev/scull0 echo hello > /dev/scull0主设备号 249 要和启动日志里注册的一致,搜索串口输出里的:
[川]第1个设备(共4个):(249,0)echo执行后,GDB 会命中scull_write断点:
(gdb) print count (gdb) print *f_pos (gdb) btbt能看到调用栈从系统调用一路到scull_write,这就是「应用 → 驱动接口 → 内核」的完整路径。
4.3 验证 scull_read
同样在 GDB 里:
(gdb) break scull_read (gdb) continue串口里执行:
cat /dev/scull0命中断点后查看:
(gdb) print count (gdb) print *f_pos (gdb) finishfinish执行完scull_read,串口会打印出之前写入的内容。如果内容正确,说明读写链路都通了。
4.4 用 TaoToken 管理调试中的模型调用
调试过程中如果用到编辑器 AI 辅助或脚本调用模型,统一走 TaoToken:
# 模型对话入口 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite # 长期编码 / Agent 场景 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite在脚本里调用时,base_url 指向https://taotoken.net/api,Key 用环境变量TAOTOKEN_API_KEY。这样调试环境里的模型调用配置只有一处,换 Key 不用改脚本。
5. 本篇常见错排查
5.1 GDB 连不上 1234 端口
现象:target remote :1234报 Connection refused。
原因通常是 QEMU 没加-s,或者-S没加导致已经跑过了。检查启动命令里有没有-s -S。如果端口被占用,换端口:
qemu-system-x86_64 ... -gdb tcp::1235 -SGDB 里对应:
target remote :12355.2 断点是灰的,打不上
现象:GDB 里break scull_write提示Cannot access memory at address。
原因是内核没开CONFIG_DEBUG_INFO,或者 KASLR 没关。回到make menuconfig确认:
Kernel hacking -> Compile-time checks and compiler options [*] Compile the kernel with debug info Processor type and features [ ] Randomize the address of the kernel image (KASLR)改完重新编译,复制新的vmlinux和bzImage。
5.3 mknod 后 echo 报 No such device
现象:echo hello > /dev/scull0提示No such device or address。
先确认主设备号对不对。串口启动日志里搜第1个设备,拿到实际主设备号。如果日志里是 249,但mknod写的是别的号,就会失败。重新创建:
rm /dev/scull0 mknod /dev/scull0 c 249 0如果主设备号是动态分配的,每次启动可能变,建议在scull_init_module里固定scull_major,或者写个启动脚本自动mknod。
5.4 模块没编译进内核
现象:启动日志里没有scull_init_module的 printk。
检查drivers/Makefile里有没有obj-y += ldd3/,以及drivers/Kconfig里有没有source "drivers/ldd3/Kconfig"。改完要重新make menuconfig确认SCULL是y或m,然后完整编译一次。
5.5 串口输出和 shell 混在一起
这是正常现象,printk 和 shell 共用 ttyS0。如果觉得乱,可以在 GDB 里看dmesg,或者把 printk 级别调高:
echo 8 > /proc/sys/kernel/printk5.6 增量编译没生效
现象:改了main.c但断点行为没变。
确认make是在内核根目录执行的,不是子目录。另外检查.config里CONFIG_SCULL的值:
grep CONFIG_SCULL .config如果是m,模块是单独编译的,不会链进vmlinux,GDB 里符号可能对不上。改成y重新编译。
6. 把调试链路固定下来
这套环境搭好之后,建议把 QEMU 启动命令写成一个脚本,比如run-qemu.sh,把-s -S和端口参数都固定进去。GDB 那边也写一个gdb.cmd,里面放target remote :1234和常用断点,启动时gdb -x gdb.cmd vmlinux就行。
模型调用配置统一走 TaoToken 的环境变量,编辑器插件、脚本、agent 共用一套 Key。需要看接入方式的话,API Keys 和文档入口:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite调试内核驱动最耗时间的往往不是写代码,而是环境配置对不上。把 QEMU 参数、内核配置、设备节点这三块固定成模板,下次换驱动只改main.c和 Makefile,链路本身不用再动。