1. 调试环境搭建:Linux内核编译与模块加载、QEMU虚拟机调试环境、JTAG/SWD硬件调试器配置
调试环境搭建,是驱动开发的第一道坎。说实话,我见过太多人卡在这一步——环境没配好,后面所有分析都像在黑暗中摸索。今天我把这些年踩过的坑、积累的经验,一次性说清楚。
1.1 Linux内核编译与模块加载
内核编译这事儿,说白了就是给你的驱动准备一个「舞台」。舞台搭不好,演员(驱动)再优秀也白搭。
1.1.1 获取内核源码
我个人习惯用主线内核,因为文档全、社区活跃。你可以从 kernel.org 下载,或者直接用发行版的内核源码包:
# Ubuntu/Debian
sudo apt-get install linux-source
cd /usr/src
tar -xjf linux-source-*.tar.bz2
# 或者直接克隆主线
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
/home/<user>/workspace/kernel 下维护一个本地仓库,方便随时切换分支。别放在 /usr/src 下,普通用户权限不够,每次编译都要 sudo,很烦。
1.1.2 配置与编译
配置内核,我建议从当前系统的配置开始,这样能保证硬件兼容性:
# 使用当前系统配置
cp /boot/config-$(uname -r) .config
make olddefconfig
# 开启调试选项(这一步很关键)
make menuconfig
在 menuconfig 里,必须打开以下选项:
| 选项 | 路径 | 说明 |
|---|---|---|
CONFIG_DEBUG_KERNEL |
Kernel hacking → Kernel debugging | 内核调试总开关 |
CONFIG_DEBUG_INFO |
Kernel hacking → Compile-time checks → Debug info | 生成调试符号(必须!) |
CONFIG_KGDB |
Kernel hacking → KGDB | 内核调试器支持 |
CONFIG_KPROBES |
General setup → Kprobes | 动态插桩,调试利器 |
CONFIG_DYNAMIC_DEBUG |
Kernel hacking → Dynamic debugging | 动态控制打印 |
CONFIG_DEBUG_INFO,结果 gdb 里全是 ??,根本看不到源码和变量名。白白浪费了三个小时。记住,调试符号是驱动调试的命根子。
编译命令很简单:
make -j$(nproc) # 并行编译,快很多
sudo make modules_install
sudo make install
重启后选择新内核。验证一下:
uname -r
cat /proc/config.gz | gunzip | grep DEBUG_INFO
1.1.3 模块加载与卸载
驱动开发中,模块加载是家常便饭。我常用的命令:
# 加载模块
sudo insmod my_driver.ko
# 查看模块信息
lsmod | grep my_driver
modinfo my_driver.ko
# 卸载模块
sudo rmmod my_driver
# 查看内核日志
dmesg | tail -20
调试小技巧: 我习惯在 insmod 之前先执行 dmesg -c 清空日志,这样加载后只看新输出,不会被旧日志干扰。
1.2 QEMU虚拟机调试环境
QEMU 是我最推荐的调试环境。为什么?因为你在宿主机上就能完成所有工作,不用反复烧写、重启真机。我早期做嵌入式驱动时,每次烧写要等 3 分钟,一天下来光等就浪费两小时。用 QEMU 后,效率翻倍。
1.2.1 安装 QEMU
sudo apt-get install qemu-system-arm qemu-system-x86 qemu-utils
我个人常用 ARM 和 x86 两种架构。ARM 用来模拟嵌入式设备,x86 用来调试 PCIe、ACPI 等桌面/服务器驱动。
1.2.2 创建根文件系统
你需要一个最小的根文件系统。我推荐用 Buildroot 或直接下载现成的:
# 下载最小根文件系统
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
# 或者用 Buildroot 自己构建
git clone https://git.buildroot.net/buildroot
cd buildroot
make qemu_arm_versatile_defconfig
make
1.2.3 启动 QEMU 并连接 GDB
这是核心操作。启动 QEMU 时加上 -s 和 -S 参数:
qemu-system-arm \
-M versatilepb \
-kernel vmlinuz \
-initrd initrd.img \
-append "root=/dev/ram console=ttyAMA0" \
-nographic \
-s -S
参数说明:
-s:在 1234 端口开启 GDB 服务(等价于-gdb tcp::1234)-S:启动时暂停 CPU,等待 GDB 连接
然后在另一个终端启动 GDB:
gdb-multiarch vmlinux
(gdb) target remote :1234
(gdb) continue
set architecture arm,然后 lx-symbols 加载模块符号。这样调试模块时,断点可以直接打在函数名上,不用算偏移。
1.2.4 调试内核模块
在 QEMU 里加载模块后,GDB 里执行:
(gdb) lx-symbols /path/to/my_driver.ko
(gdb) break my_driver_init
(gdb) continue
当模块初始化时,GDB 会自动停在断点处。你可以单步、查看变量、甚至修改内存。
1.3 JTAG/SWD硬件调试器配置
当 QEMU 不够用,或者你需要调试真实硬件时,JTAG/SWD 调试器就派上用场了。这东西说白了就是一根「探针」,直接插到芯片的调试接口上,让你能控制 CPU、读写内存、设置断点。
1.3.1 常见调试器对比
| 调试器 | 接口 | 速度 | 价格 | 适用场景 |
|---|---|---|---|---|
| J-Link | JTAG/SWD | 最高 50 MHz | 中高 | ARM Cortex 系列,稳定可靠 |
| ST-Link | SWD | 最高 4 MHz | 低 | STM32 系列,性价比高 |
| OpenOCD + FT2232 | JTAG/SWD | 取决于硬件 | 低 | 开源方案,灵活但配置复杂 |
| Lauterbach | JTAG/SWD | 最高 100 MHz | 高 | 工业级,功能最全 |
我个人最常用 J-Link,因为它兼容性好、速度快,而且 GDB 服务器集成得很完善。
1.3.2 硬件连接
SWD 只需要 4 根线:
- SWDIO:数据线
- SWCLK:时钟线
- GND:地线
- VREF:参考电压(可选,用于电平匹配)
JTAG 需要 5 根:
- TMS:模式选择
- TCK:时钟
- TDI:数据输入
- TDO:数据输出
- nTRST:复位(可选)
1.3.3 软件配置
以 J-Link 为例,配置 GDB 服务器:
# 启动 J-Link GDB 服务器
JLinkGDBServer -device STM32F407VG -if SWD -speed 4000 -port 2331
# 在另一个终端启动 GDB
arm-none-eabi-gdb vmlinux
(gdb) target remote :2331
(gdb) monitor reset
(gdb) continue
用 OpenOCD 的话,配置文件大概长这样:
# openocd.cfg
source [find interface/stlink.cfg]
source [find target/stm32f4x.cfg]
adapter speed 4000
reset_config srst_only
启动命令:
openocd -f openocd.cfg
# 然后在另一个终端
telnet localhost 4444
> reset halt
> flash write_image erase my_driver.bin 0x08000000
> reset run
adapter speed,结果默认速度太低,调试器响应慢得像蜗牛。后来我把速度从 100 kHz 调到 4 MHz,整个世界都流畅了。但注意,速度不是越高越好——线太长、干扰大的时候,高速会导致通信错误。我一般从 1 MHz 开始试,稳定后再往上加。
1.4 本章知识体系
下面这张图,把三种调试环境的关系和适用场景梳理清楚了:
三种环境各有侧重。我的建议是:先用 QEMU 把驱动逻辑调通,再用 JTAG/SWD 在真机上验证。内核编译是贯穿始终的基础,不管用哪种环境,都得先把内核配好、编好。
调试环境搭建好了,后面的事情就顺了。记住,磨刀不误砍柴工——花半天时间把环境配扎实,后面调试时能省下几天时间。