内核驱动调试接口与使用方法入门(转)

发布时间:2026/5/19 18:09:19

内核驱动调试接口与使用方法入门(转) 这个文章比较详细的列举了内核调试的各种手段和方法。1. 引言驱动开发的真正挑战往往不在于编写代码而在于调试Debugging。在用户态程序开发中拥有 GDB、IDE 断点和printf等简单方便的调试工具但在内核态开发中环境变得严苛且不透明。以下是内核驱动开发中常见的一些问题模块加载失败insmod报错但是不知错在哪里。Probe 未触发确认代码写的没问题但是驱动入口函数就是不执行。数据异常I2C/SPI 读回来的数据全是0xFF或乱码。中断怪象IRQ 触发了但执行结果与期望不符。内存踩踏写越界导致随机崩溃且崩溃点往往不在案发现场。系统崩溃一个空指针解引用整个系统直接崩溃。内核态程序运行时无法随意暂停系统。因此我们需要构建一套从日志记录到实时追踪再到现场分析的完整工具箱。本文将系统梳理 Linux 内核驱动开发中最高频、最实用的调试接口与方法。2. 调试工具箱在动手之前根据问题现象选择合适的工具能事半功倍。调试工具箱3. 基础日志内核打印是第一道防线但滥用printk会导致日志洪水养成良好的打印习惯能让调试事半功倍。同时学会使用断言能提前暴露问题。3.1 规范化输出在驱动代码中建议放弃裸写printk改用内核推荐的等级宏这有助于利用内核日志等级过滤无关信息。为了让日志统一带上驱动名称可以在文件最开头定义pr_fmt。/* 放在 .c 文件开头自动给所有日志加上前缀 */ #define pr_fmt(fmt) MyDriver: fmt /* 自带日志级别且易于搜索 */ pr_info(module loaded\n); pr_err(i2c timeout (Addr: 0x%02x)\n, addr); /* 使用 %pS 可以打印函数名适合调试回调函数 */ pr_info(callback function is %pS\n, callback_func); /* 调试专用平时不打印开启 DEBUG 宏后才生效 */ pr_debug(count %d\n, count);实用技巧查看日志时可根据需要选用dmesg -w实时跟随模式和dmesg -C清空缓冲区提高阅读效率。3.2 频控打印在中断处理函数或高频循环中打印日志会导致系统卡顿甚至挂死。这时候推荐使用_ratelimited后缀的打印宏。/* 每秒最多打印默认次数通常是 5 秒 10 次具体阈值由内核实现决定不同内核版本可能不同 */ if (status_error) pr_err_ratelimited(hardware error: 0x%x\n, status);3.3 动态开关如果需要临时开启pr_debug级别的日志可以使用内核的动态调试功能需开启CONFIG_DYNAMIC_DEBUG宏控。代码中保留pr_debug默认不打印运行时通过命令精准开启无需重新编译内核。3.3.1 开启与关闭通过debugfs接口动态控制# 挂载 debugfs (通常系统会自动挂载) sudo mount -t debugfs none /sys/kernel/debug # 开启指定文件名 (p) sudo sh -c echo file my_driver.c p /sys/kernel/debug/dynamic_debug/control # 开启指定行号 sudo sh -c echo file my_driver.c line 123 p /sys/kernel/debug/dynamic_debug/control # 关闭指定文件名 (-p) sudo sh -c echo file my_driver.c -p /sys/kernel/debug/dynamic_debug/control3.3.2 加载时开启如果驱动在加载瞬间就报错来不及通过debugfs接口写入参数时可以使用模块参数# 整个模块 sudo modprobe my_driver dyndbgmodule my_driver p # 指定文件 sudo modprobe my_driver dyndbgfile my_driver.c p3.4 数据缓冲区打印驱动开发常涉及协议调试如 SPI/I2C 数据包不要自己写for循环打印内核提供了标准接口print_hex_dump。/* 打印一段内存 buffer * KERN_DEBUG: 日志等级 * RX_DATA: : 前缀字符串 * DUMP_PREFIX_OFFSET: 显示偏移量 * 16: 每行显示 16 字节 * 1: 每个数据单元为 1 字节 * buf: 数据指针 * len: 数据长度 * true: 是否显示 ASCII 字符 * 打印效果RX_DATA: 00000000: 1a 2b 3c 4d ... .M */ print_hex_dump(KERN_DEBUG, RX_DATA: , DUMP_PREFIX_OFFSET, 16, 1, buf, len, true);3.5 断言在关键路径上检查条件是防御性编程的基础。WARN_ON(condition)如果条件为真打印堆栈信息程序继续运行。适用于不应该发生但发生了也能勉强运行的场景。BUG_ON(condition)如果条件为真打印堆栈信息并引发 Kernel Panic系统崩溃。仅适用于如果继续运行会破坏核心数据结构或造成硬件损坏的极端场景。注意驱动开发中应尽量使用WARN_ON并配合错误码返回如return -EINVAL慎用BUG_ON因为它会导致整个系统不可用。4. debugfs虚拟文件系统中sysfs用于建立规范的标准设备模型procfs用于观测系统运行信息。如果你只是想查看驱动内部的变量如寄存器值、统计计数、状态机状态等更加自由的debugfs是最佳选择。4.1 代码示意#include linux/debugfs.h static struct dentry *dbg_dir; static u32 irq_counter 0; static u64 last_timestamp 0; static int __init my_driver_init(void) { // 1. 创建目录 /sys/kernel/debug/my_driver/ dbg_dir debugfs_create_dir(my_driver, NULL); if (!dbg_dir) return -ENOMEM; // 2. 创建文件将变量暴露给用户空间只读 // u32 打印的是十进制x64 打印的是十六进制 debugfs_create_u32(irq_count, 0444, dbg_dir, irq_counter); debugfs_create_x64(last_ts, 0444, dbg_dir, last_timestamp); return 0; } static void __exit my_driver_exit(void) { // 递归删除目录及其下文件 debugfs_remove_recursive(dbg_dir); }4.2 实时监控在用户态配合watch命令形成简易仪表盘# 每 0.5 秒刷新一次数据 sudo watch -n 0.5 cat /sys/kernel/debug/my_driver/irq_count5. trace_printk当你在调试中断延迟、调度时序或原子上下文问题时普通的printk可能会因为 I/O 慢而改变时序甚至掩盖 BugHeisenbug。此时我们需要 ftrace 子系统提供的trace_printk它的特点是只把日志写入内存缓冲区不走虚拟终端、伪终端或串口等 TTY I/O 接口速度极快可在中断上下文放心使用。5.1 架构对比printk 和 trace_printk 对比5.2 使用方法代码插桩/* 极其轻量可用于中断上下文几乎不影响时序 */ trace_printk(ISR enter: status0x%x\n, reg_val);查看记录# 1. 开启追踪 (确认内核已开启 CONFIG_FTRACE) sudo sh -c echo 1 /sys/kernel/debug/tracing/tracing_on # ... 运行你的程序 ... # 2. 读取追踪缓冲区 sudo cat /sys/kernel/debug/tracing/trace # 3. 关闭追踪 sudo sh -c echo 0 /sys/kernel/debug/tracing/tracing_on说明trace_printk主要开销来自缓冲区写入关闭后几乎没有开销。建议仅在调试时启用避免在生产环境影响性能。6. devmem有时候检查代码没有问题但是硬件寄存器没写进去或者想直接读硬件状态。这时候我们不需要反复修改读写寄存器的代码可以直接用内存地址读写工具devmem2。devmem是 BusyBox 提供的简化版工具devmem2是独立实现的完整工具两者功能类似这里以devmem2为例说明。直接通过物理地址读写寄存器可能导致硬件状态异常或系统崩溃请注意在操作前确认地址正确性。# 安装 devmem2以 Debian 系发行版为例 sudo apt install devmem2 # 读取指定物理地址的值4 字节长度 sudo devmem2 0x00200000 w # 向指定物理地址写值1 字节长度 sudo devmem2 0x00200000 b 0x1读写操作日志如下所示$ sudo devmem2 0x9c0000000 b /dev/mem opened. Memory mapped at address 0xffff89399000. Value at address 0xC0000000 (0xffff89399000): 0xFF $ sudo devmem2 0x9c0000000 b 0x1 /dev/mem opened. Memory mapped at address 0xffffa7625000. Value at address 0xC0000000 (0xffffa7625000): 0xFF Written 0x1; readback 0xFF7. Oops当发生空指针引用时内核会打印 Oops 信息。调试内核或内核模块时分别需要加上CONFIG_DEBUG_INFOy或EXTRA_CFLAGS -g打开调试信息。检查文件中是否包含调试相关的段如果没有 debug 相关输出说明没有打开调试信息$ aarch64-linux-gnu-objdump -h hello_device.ko | grep debug 20 .debug_info 00000750 0000000000000000 0000000000000000 00000640 2**0 21 .debug_abbrev 0000017b 0000000000000000 0000000000000000 00000d90 2**0 22 .debug_aranges 00000040 0000000000000000 0000000000000000 00000f0b 2**0 23 .debug_rnglists 00000021 0000000000000000 0000000000000000 00000f4b 2**0 24 .debug_line 00000108 0000000000000000 0000000000000000 00000f6c 2**0 25 .debug_str 00000fd6 0000000000000000 0000000000000000 00001074 2**0 26 .debug_line_str 000001f9 0000000000000000 0000000000000000 0000204a 2**0 27 .debug_frame 00000060 0000000000000000 0000000000000000 00002248 2**37.1 解读 OopsOops 日志中最关键的是PC指针和 Call Trace[27580.005612] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000 [27580.006488] Mem abort info: [27580.006872] ESR 0x0000000096000044 [27580.007251] EC 0x25: DABT (current EL), IL 32 bits [27580.007772] SET 0, FnV 0 [27580.008085] EA 0, S1PTW 0 [27580.008409] FSC 0x04: level 0 translation fault [27580.008891] Data abort info: [27580.009187] ISV 0, ISS 0x00000044 [27580.009572] CM 0, WnR 1 [27580.009875] user pgtable: 4k pages, 48-bit VAs, pgdp00000004f1c40000 [27580.010552] [0000000000000000] pgd0000000000000000, p4d0000000000000000 [27580.011236] Internal error: Oops: 0000000096000044 [#1] SMP [27580.011778] Modules linked in: hello_device(O) led_class_multicolor btsdio brcmfmac brcmutil panfrost pwm_fan rk805_pwrkey drm_shmem_helper gpu_sched zram zsmalloc binfmt_misc sch_fq_codel fuse dm_mod nfnetlink ip_tables ipv6 nvmem_rockchip_otp yt6801 rockchip_cpuinfo uio_pdrv_genirq uio [last unloaded: hello_device(O)] [27580.014673] CPU: 1 PID: 3512 Comm: insmod Tainted: G O 6.1.75-vendor-rk35xx #1 [27580.015501] Hardware name: Orange Pi 5 Pro (DT) [27580.015952] pstate: 60400009 (nZCv daif PAN -UAO -TCO -DIT -SSBS BTYPE--) [27580.016631] pc : hello_init0x24/0x1000 [hello_device] [27580.017171] lr : hello_init0x20/0x1000 [hello_device] [27580.017700] sp : ffff800011233af0 [27580.018038] x29: ffff800011233af0 x28: ffff80000a29a990 x27: 0000000000000000 [27580.018753] x26: ffff800011233cb0 x25: 0000000000000000 x24: 0000000000000000 [27580.019465] x23: 0000000000000000 x22: 0000000000000000 x21: ffff800001364058 [27580.020174] x20: ffff80000a4ddfe0 x19: ffff800001252000 x18: 0000000000000000 [27580.020884] x17: 0000000000000000 x16: 0000000000000000 x15: 0000000000000000 [27580.021593] x14: 0000000000000000 x13: 0000000000000000 x12: 0000000000000000 [27580.022303] x11: 0000000000000000 x10: 0000000000000000 x9 : ffff8000081a06d4 [27580.023013] x8 : 00000db8c200000c x7 : 21646c726f57206f x6 : 6f57206f6c6c6548 [27580.023722] x5 : 0000000000000000 x4 : 0000000000000000 x3 : 0000000000000000 [27580.024431] x2 : 0000000000000000 x1 : ffff0004f08cadc0 x0 : 0000000000000000 [27580.025143] Call trace: [27580.025399] hello_init0x24/0x1000 [hello_device] [27580.025909] do_one_initcall0x94/0x1e4 [27580.026323] do_init_module0x58/0x1e0 [27580.026724] load_module0x1850/0x1918 [27580.027117] __do_sys_finit_module0xf8/0x118 [27580.027563] __arm64_sys_finit_module0x24/0x30 [27580.028032] invoke_syscall0x8c/0x128 [27580.028425] el0_svc_common.constprop.00xd8/0x128 [27580.028911] do_el0_svc0xac/0xbc [27580.029261] el0_svc0x2c/0x54 [27580.029590] el0t_64_sync_handler0xac/0x13c [27580.030026] el0t_64_sync0x19c/0x1a0 [27580.030402] PC: 0xffff800001252024: [27580.030892] 1e24 ******** ******** ******** ******** ******** ******** ******** ******** [27580.031771] 1e44 ******** ******** ******** ******** ******** ******** ******** ********7.2 快速定位7.2.1 decode_stacktrace.shLinux 内核源码树提供了一个强大的脚本scripts/decode_stacktrace.sh它可以自动加载 vmlinux 和 ko 文件把 Oops 日志里的十六进制直接翻译成代码行号。如果.ko文件没有开启调试信息执行脚本会报错WARNING! Modules path isnt set, but is needed to parse this symbol。# 语法将 dmesg 输出通过管道传给脚本 # 需要带调试符号的 vmlinux以及模块.ko所在目录 sudo dmesg | ./scripts/decode_stacktrace.sh vmlinux auto /home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/解析后偏移量信息被转换成了文件名:行号参见如下日志[ 114.248142] pc : hello_init (/home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/hello_device.c:9) hello_device [ 114.248635] lr : hello_init (/home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/hello_device.c:9) hello_device [ 114.249118] sp : ffff800011263af0 [ 114.249431] x29: ffff800011263af0 x28: ffff80000a29a990 x27: 0000000000000000 [ 114.256038] Call trace: [ 114.256270] hello_init (/home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/hello_device.c:9) hello_device7.2.2 faddr2line内核源码提供了一个脚本工具faddr2line它支持函数偏移语法省去了手动查nm和计算的过程这是目前内核开发者最常用的方式。# 格式faddr2line ko文件 函数名偏移 ./scripts/faddr2line hello_device.ko hello_init0x24执行结果如下偏移地址是0x24函数总长度是0x34hello_init0x24/0x34: hello_init at /home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/hello_device.c:97.3 手动定位如果你无法使用自动化脚本可以手动计算偏移量。这里假设 Oops 显示hello_init0x24/0x1000表示函数内偏移为 0x24总长度为 0x1000按页对齐。1. 获取函数的起始地址使用nm工具从.ko文件中提取符号的基地址$ nm hello_device.ko | grep hello_init 0000000000000000 t hello_init2. 计算绝对偏移量将nm查到的基地址0x0000000000000000与 Oops 中的偏移量0x24相加即绝对偏移量为0x24。3. 使用 addr2line 定位源码行号如果没有开启调试信息.ko文件中不会包含源码行号映射表行号信息就会变成问号。# -f: 显示函数名, -e: 指定文件 addr2line -f -e hello_device.ko 0x24定位结果如下所示可以看到函数名是hello_init代码行号是第 9 行hello_init /home/dump_linux/learning_linux_kernel_driver_from_scratch/hello_device/hello_device.c:98. KASAN如果你的驱动遇到莫名其妙的随机崩溃或者写了一个变量却改变了另一个无关变量的值大概率是内存越界或释放后使用。这种情况是最难调试的 Bug因为崩溃点通常不是案发现场。解决方案是开启KASAN (Kernel Address Sanitizer)需要在内核配置时打开配置选项这里仅供参考相关配置项较多具体请根据需要调整CONFIG_KASANy CONFIG_KASAN_GENERICy CONFIG_HAVE_ARCH_KASANy重新编译内核后KASAN 会自动监控所有内存访问在每次内存访问时进行检查。一旦越界它会立刻打印报错并指出谁在非法访问函数、行号。这块内存是谁分配的分配时的堆栈。这块内存是谁释放的释放后使用。典型 KASAN 报告如下所示:BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right0xa8/0xbc [kasan_test] Write of size 1 at addr ffff8801f44ec37b by task insmod/2760 CPU: 1 PID: 2760 Comm: insmod Not tainted 4.19.0-rc3 #698 Hardware name: QEMU Standard PC (i440FX PIIX, 1996), BIOS 1.10.2-1 04/01/2014 Call Trace: dump_stack0x94/0xd8 print_address_description0x73/0x280 kasan_report0x144/0x187 __asan_report_store1_noabort0x17/0x20 kmalloc_oob_right0xa8/0xbc [kasan_test] kmalloc_tests_init0x16/0x700 [kasan_test] do_one_initcall0xa5/0x3ae do_init_module0x1b6/0x547 load_module0x75df/0x8070 __do_sys_init_module0x1c6/0x200 __x64_sys_init_module0x6e/0xb0 do_syscall_640x9f/0x2c0 entry_SYSCALL_64_after_hwframe0x44/0xa9 RIP: 0033:0x7f96443109da RSP: 002b:00007ffcf0b51b08 EFLAGS: 00000202 ORIG_RAX: 00000000000000af RAX: ffffffffffffffda RBX: 000055dc3ee521a0 RCX: 00007f96443109da RDX: 00007f96445cff88 RSI: 0000000000057a50 RDI: 00007f9644992000 RBP: 000055dc3ee510b0 R08: 0000000000000003 R09: 0000000000000000 R10: 00007f964430cd0a R11: 0000000000000202 R12: 00007f96445cff88 R13: 000055dc3ee51090 R14: 0000000000000000 R15: 0000000000000000 Allocated by task 2760: save_stack0x43/0xd0 kasan_kmalloc0xa7/0xd0 kmem_cache_alloc_trace0xe1/0x1b0 kmalloc_oob_right0x56/0xbc [kasan_test] kmalloc_tests_init0x16/0x700 [kasan_test] do_one_initcall0xa5/0x3ae do_init_module0x1b6/0x547 load_module0x75df/0x8070 __do_sys_init_module0x1c6/0x200 __x64_sys_init_module0x6e/0xb0 do_syscall_640x9f/0x2c0 entry_SYSCALL_64_after_hwframe0x44/0xa9 Freed by task 815: save_stack0x43/0xd0 __kasan_slab_free0x135/0x190 kasan_slab_free0xe/0x10 kfree0x93/0x1a0 umh_complete0x6a/0xa0 call_usermodehelper_exec_async0x4c3/0x640 ret_from_fork0x35/0x40 The buggy address belongs to the object at ffff8801f44ec300 which belongs to the cache kmalloc-128 of size 128 The buggy address is located 123 bytes inside of 128-byte region [ffff8801f44ec300, ffff8801f44ec380) The buggy address belongs to the page: page:ffffea0007d13b00 count:1 mapcount:0 mapping:ffff8801f7001640 index:0x0 flags: 0x200000000000100(slab) raw: 0200000000000100 ffffea0007d11dc0 0000001a0000001a ffff8801f7001640 raw: 0000000000000000 0000000080150015 00000001ffffffff 0000000000000000 page dumped because: kasan: bad access detected Memory state around the buggy address: ffff8801f44ec200: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb ffff8801f44ec280: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc ffff8801f44ec300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ^ ffff8801f44ec380: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb ffff8801f44ec400: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc9. 总结场景推荐工具核心优势日常逻辑验证pr_info / pr_debug / pr_fmt简单直接配合 dynamic_debug 灵活开关数据包分析print_hex_dump格式化输出十六进制 Buffer美观易读防御性检查WARN_ON暴露逻辑错误但不至于让系统崩溃查看内部状态debugfs干净不污染 dmesg支持按需读取中断/时序分析trace_printk极低开销不影响系统实时性硬件物理验证devmem / devmem2绕过驱动直接读写物理寄存器内核崩溃faddr2line / decode_stacktrace.sh将晦涩的内存地址转换为具体的代码行号内存越界/踩踏KASAN捕捉非法内存 Bug 的神器调试驱动是驱动开发的必备技能调试不能靠运气得靠基于证据的推理。

相关新闻