ESC
输入关键词搜索文章标题和内容

Linux驱动调试七大杀器,从printk到KASAN全打通

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

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定位源码行:

来自 linuxros.cn · linuxROS
# 内核中的地址,用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 标准调试流程:七步闭环,从加载到验收

把上面六个工具串起来,就是一套完整的驱动调试流程:

  1. 加载驱动:insmod xxx.ko
  2. 看日志:dmesg -w查看printk输出,排查初始化错误
  3. 查状态:cat /proc/modules、ls /sys/class/xxx确认设备注册成功
  4. 硬件调试:devmem 0xXX读写寄存器,验证硬件响应
  5. 功能测试:用户态程序调用ioctl,测试驱动功能
  6. 崩溃排查:Oops + addr2line定位崩溃代码行
  7. 稳定性测试:开启KASAN,检测内存越界和使用后释放

三、调试工具对比:printk vs procfs vs devmem vs Oops vs KASAN vs ioctl

工具 调试层级 需要改代码 适用阶段 定位精度
printk+dmesg 日志层 是 全阶段 函数级
procfs 系统状态层 否 运行时 模块级
sysfs 设备状态层 否 运行时 设备级
devmem 硬件层 否 硬件调试 寄存器级
Oops+addr2line 崩溃层 否 崩溃后 代码行级
KASAN 内存层 需重编内核 稳定性测试 代码行级
ioctl 交互层 是 功能测试 接口级

四、核心流程图:驱动调试全链路

flowchart TB A(["开始调试"]) --> B["insmod加载驱动"] B --> C["dmesg -w<br/>查看初始化日志"] C --> D{"初始化成功?"} D -->|"否"| E["检查printk输出<br/>定位初始化失败原因"] E --> F{"硬件问题?"} F -->|"是"| G["devmem读写寄存器<br/>验证硬件响应"] F -->|"否"| H["检查代码逻辑<br/>修复后重新加载"] G --> I{"寄存器正确?"} I -->|"否"| J["排查硬件连线<br/>和设备树配置"] I -->|"是"| H J --> H D -->|"是"| K["cat /proc/modules<br/>ls /sys/class/xxx<br/>确认设备注册"] K --> L["ioctl测试<br/>驱动功能"] L --> M{"功能正常?"} M -->|"否"| N["检查ioctl命令<br/>和参数传递"] N --> H M -->|"是"| O["开启KASAN<br/>稳定性测试"] O --> P{"内存问题?"} P -->|"是"| Q["KASAN报告<br/>定位越界/泄漏行"] Q --> H P -->|"否"| R{"崩溃?"} R -->|"是"| S["Oops+addr2line<br/>定位崩溃代码行"] S --> H R -->|"否"| T(["调试完成"]) style A fill:#E3F2FD,stroke:#1976D2 style T fill:#E8F5E9,stroke:#388E3C style E fill:#FFF8E1,stroke:#F57C00 style G fill:#F3E5F5,stroke:#7B1FA2 style Q fill:#FFEBEE,stroke:#D32F2F style S fill:#FFEBEE,stroke:#D32F2F

五、常见问题解决

问题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%的驱动问题都能在半小时内定位到根因。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页