Linux驱动调试七大杀器,从printk到KASAN全打通
导读:Linux驱动调试只会加printk?那是入门水平。本文从内核日志、虚拟文件系统、硬件寄存器直读、Oops崩溃定位、内存越界检测到驱动交互调试,七大工具逐层递进,覆盖90%驱动调试场景,看完直接上手。
一、原理简析:Linux驱动调试为什么难
驱动跑在内核态,没有GDB直接调试的便利,一个空指针就能让整个系统崩掉。调试的核心思路就三条:看日志、查状态、抓现场。
看日志靠printk和dmesg,这是最基础的信号来源。查状态靠procfs和sysfs,不用改代码就能窥探内核和硬件的实时状态。抓现场靠Oops信息和KASAN,崩溃后直接锁定代码行。
三者配合,构成从"发现问题"到"定位根因"的完整链路。devmem是硬件调试的旁路手段,ioctl是驱动功能验证的交互接口。理解这条链路,比记住一百个命令更有用。
二、实操步骤:七大调试工具逐个拆解
2.1 内核日志:printk+dmesg,驱动调试第一刀
printk是内核态的printf,带日志级别控制输出。根据Linux内核官方文档(kernel.org/doc/html/latest/core-api/printk-basics.html),8个级别从0到7,数字越小优先级越高:
#include <linux/printk.h>
/* 8个日志级别,优先级从高到低 */
printk(KERN_EMERG "系统不可用\n"); /* 0 - 紧急 */
printk(KERN_ALERT "必须立即处理\n"); /* 1 - 警报 */
printk(KERN_CRIT "严重条件\n"); /* 2 - 严重 */
printk(KERN_ERR "错误条件\n"); /* 3 - 错误 */
printk(KERN_WARNING "警告条件\n"); /* 4 - 警告 */
printk(KERN_NOTICE "正常但重要\n"); /* 5 - 通知 */
printk(KERN_INFO "提示信息\n"); /* 6 - 信息 */
printk(KERN_DEBUG "调试信息\n"); /* 7 - 调试 */
/* 推荐用pr_*宏,更简洁 */
pr_info("驱动初始化成功,版本:%d\n", 10);
pr_err("寄存器写入失败!\n");
pr_debug("变量 val = %#x\n", val);
pr_debug默认不输出,需要定义DEBUG宏或开启CONFIG_DYNAMIC_DEBUG才生效。这是新手最常踩的坑——加了pr_debug却看不到输出,不是代码没跑,是级别没开。
dmesg读取内核环形缓冲区中的printk日志:
dmesg # 查看全部日志
dmesg -w # 实时跟踪(类似tail -f)
dmesg -c # 查看并清空
dmesg -T # 带时间戳
dmesg | grep mydrv # 过滤自己的驱动
控制台日志级别通过cat /proc/sys/kernel/printk查看,4个值分别是当前、默认、最小、启动时默认级别。echo 8 > /proc/sys/kernel/printk或dmesg -n 8可以把所有级别日志都输出到控制台。
2.2 免代码查状态:procfs+sysfs,不动代码也能看底牌
procfs(/proc)和sysfs(/sys)是两个虚拟文件系统,不需要修改任何代码,直接cat就能看内核和硬件状态。
procfs核心路径:
cat /proc/cpuinfo # CPU信息
cat /proc/meminfo # 内存状态
cat /proc/modules # 已加载内核模块
cat /proc/cmdline # 内核启动参数
cat /proc/interrupts # 中断分配(排查中断问题必看)
cat /proc/kmsg # 实时内核日志
cat /proc/[PID]/maps # 进程内存映射
sysfs核心路径:
ls /sys/class/ # 设备类(gpio、i2c、tty等)
ls /sys/devices/ # 所有硬件设备
ls /sys/module/ # 内核模块信息
# GPIO调试实战
cat /sys/class/gpio/gpio10/value # 读GPIO电平
echo 1 > /sys/class/gpio/gpio10/value # 写GPIO电平
# 查看驱动绑定状态
cat /sys/bus/platform/drivers/xxx_driver/xxx_device
/proc/interrupts是排查驱动中断问题的第一站。如果驱动注册了中断但计数始终为0,说明中断没进来,查硬件连线和设备树配置。
2.3 硬件寄存器直调:devmem,跳过驱动直接跟硬件对话
devmem是嵌入式调试神器,通过/dev/mem直接读写物理地址,不需要驱动。busybox自带,格式:
# 格式:devmem 物理地址 [位宽] [值]
devmem 0x12340000 # 读32位(默认)
devmem 0x12340000 8 # 8位读取
devmem 0x12340000 16 # 16位读取
devmem 0x12340000 32 0x55 # 写入0x55
devmem 0x12340000 64 # 64位读取
devmem的本质是打开/dev/mem,用mmap把物理地址映射到用户空间。内核配置CONFIG_DEVMEM必须开启,CONFIG_STRICT_DEVMEM默认开启会限制只能访问非RAM区域(MMIO寄存器可以,系统内存不行)。如果读出来全是0或0xFF,先检查这两个配置项。
注意:随意读写物理地址会导致系统崩溃,仅用于硬件调试,生产环境必须关闭/dev/mem访问。
2.4 内核崩溃定位:Oops信息拆解,崩溃代码一行锁定
内核崩溃时自动打印Oops信息,包含崩溃类型、PC指针、调用栈。根据kernel.org官方文档(admin-guide/bug-hunting.html),关键信息解读:
BUG: Unable to handle kernel NULL pointer dereference at virtual address 00000000
PC : [<c0123456>] test_driver+0x50/0x100
LR : [<c0654321>] 0xc0654321
Process bash (pid: 1234, stack limit: 0xcf000000)
PC(ARM)或RIP(x86_64)是崩溃指令地址,格式是函数名+偏移/函数大小。上面的test_driver+0x50/0x100表示在test_driver函数内偏移0x50处崩溃,函数总大小0x100。
用addr2line定位源码行:
# 内核中的地址,用vmlinux解析
addr2line -e vmlinux c0123456
# 输出:/driver/test/test.c:56
# 带函数名
addr2line -e vmlinux -f c0123456
# 输出:test_driver
# /driver/test/test.c:56
# 模块中的地址,用未strip的.ko文件
addr2line -e mydriver.ko 0x50
内核还提供了自动化脚本scripts/decode_stacktrace.sh,可以把整个调用栈的地址批量解析为源码位置:
./scripts/decode_stacktrace.sh vmlinux /path/to/kernel/source < oops.log
前提是内核编译时开启了CONFIG_DEBUG_INFO和CONFIG_KALLSYMS。
2.5 内存bug克星:KASAN,越界和野指针无所遁形
KASAN(Kernel Address Sanitizer)是内核级内存错误检测器,根据kernel.org官方文档(dev-tools/kasan.html),它有三种模式:
| 模式 | 配置项 | 适用场景 | 架构支持 |
|---|---|---|---|
| Generic KASAN | CONFIG_KASAN_GENERIC | 开发调试,最精准 | x86_64/arm/arm64/powerpc/riscv/s390/xtensa/loongarch |
| Software Tag-Based | CONFIG_KASAN_SW_TAGS | 内存受限设备调试 | arm64 |
| Hardware Tag-Based | CONFIG_KASAN_HW_TAGS | 生产环境在线检测 | arm64(需MTE) |
开启方法:
CONFIG_KASAN=y # 开启KASAN
CONFIG_KASAN_GENERIC=y # 通用模式(推荐调试用)
CONFIG_DEBUG_KERNEL=y # 调试总开关
CONFIG_DEBUG_INFO=y # 符号信息(配合addr2line)
CONFIG_STACKTRACE=y # 栈回溯(增强报告可读性)
Generic KASAN需要GCC 8.3.0+或Clang,内存开销约3-5倍,性能下降约50%,仅用于开发测试环境。
KASAN报告示例:
BUG: KASAN: slab-out-of-bounds in test_func+0x20/0x50
Write of size 4 at addr ffff888012345678 by task bash/1234
Allocated by task 100:
kmem_cache_alloc_trace+0x10b/0x190
test_func+0x3d/0x50
Freed by task 100:
kfree+0x80/0x120
cleanup_module+0x20/0x40
KASAN直接告诉你:越界类型(slab-out-of-bounds)、访问大小(4字节)、分配和释放的调用栈。比手动分析Oops信息高效得多。
2.6 驱动交互调试:ioctl,用户态和内核态的桥梁
ioctl是用户态程序与内核驱动交互的核心接口,用于发送命令和传递参数。
内核态驱动实现:
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#define TEST_MAGIC 'T'
#define TEST_CMD_READ _IOR(TEST_MAGIC, 1, int)
#define TEST_CMD_WRITE _IOW(TEST_MAGIC, 2, int)
long test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val;
switch (cmd) {
case TEST_CMD_READ:
/* 内核→用户:copy_to_user */
if (copy_to_user((void __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
case TEST_CMD_WRITE:
/* 用户→内核:copy_from_user */
if (copy_from_user(&val, (void __user *)arg, sizeof(val)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
/* file_operations中注册 */
static const struct file_operations test_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = test_ioctl,
};
注意:现代内核统一使用unlocked_ioctl,旧的ioctl接口已移除。命令码用_IOR/_IOW/_IOWR宏定义,包含魔数、序号和数据类型,避免命令码冲突。
用户态测试程序:
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdio.h>
#define TEST_MAGIC 'T'
#define TEST_CMD_READ _IOR(TEST_MAGIC, 1, int)
#define TEST_CMD_WRITE _IOW(TEST_MAGIC, 2, int)
int main(void)
{
int fd = open("/dev/test_dev", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
int val = 42;
ioctl(fd, TEST_CMD_WRITE, &val); /* 写入 */
ioctl(fd, TEST_CMD_READ, &val); /* 读出 */
printf("read val = %d\n", val);
close(fd);
return 0;
}
2.7 标准调试流程:七步闭环,从加载到验收
把上面六个工具串起来,就是一套完整的驱动调试流程:
- 加载驱动:
insmod xxx.ko - 看日志:
dmesg -w查看printk输出,排查初始化错误 - 查状态:
cat /proc/modules、ls /sys/class/xxx确认设备注册成功 - 硬件调试:
devmem 0xXX读写寄存器,验证硬件响应 - 功能测试:用户态程序调用ioctl,测试驱动功能
- 崩溃排查:Oops + addr2line定位崩溃代码行
- 稳定性测试:开启KASAN,检测内存越界和使用后释放
三、调试工具对比:printk vs procfs vs devmem vs Oops vs KASAN vs ioctl
| 工具 | 调试层级 | 需要改代码 | 适用阶段 | 定位精度 |
|---|---|---|---|---|
| printk+dmesg | 日志层 | 是 | 全阶段 | 函数级 |
| procfs | 系统状态层 | 否 | 运行时 | 模块级 |
| sysfs | 设备状态层 | 否 | 运行时 | 设备级 |
| devmem | 硬件层 | 否 | 硬件调试 | 寄存器级 |
| Oops+addr2line | 崩溃层 | 否 | 崩溃后 | 代码行级 |
| KASAN | 内存层 | 需重编内核 | 稳定性测试 | 代码行级 |
| ioctl | 交互层 | 是 | 功能测试 | 接口级 |
四、核心流程图:驱动调试全链路
五、常见问题解决
问题1:pr_debug加了但看不到输出
pr_debug默认不编译输出代码,需要定义DEBUG宏或开启CONFIG_DYNAMIC_DEBUG。在驱动源文件开头加#define DEBUG,或用动态调试:echo 'file mydriver.c +p' > /sys/kernel/debug/dynamic_debug/control。
问题2:devmem读出来全是0xFF
两种可能:一是CONFIG_STRICT_DEVMEM阻止了RAM区域访问,改读MMIO寄存器地址试试;二是目标地址没有被映射,检查设备树中该外设的reg属性和ranges配置。
问题3:Oops中地址显示问号或hex值
内核没有开启CONFIG_KALLSYMS,无法把地址解析为函数名。重新编译内核开启该选项,或手动用addr2line配合vmlinux解析。
六、总结
驱动调试七层递进:printk看日志、procfs/sysfs查状态、devmem调硬件、Oops定位崩溃、KASAN抓内存、ioctl测功能、标准流程串全局。printk是基本功,KASAN是稳定性保障,Oops+addr2line是崩溃定位的杀手锏。掌握这套工具链,90%的驱动问题都能在半小时内定位到根因。