《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 12 章 Linux 设备驱动的调试
参考:宋宝华 著,机械工业出版社,2015年版
12.1 GDB 调试内核
12.1.1 GDB 调试内核的基本原理
GDB(GNU Debugger)是 Linux 下最强大的调试工具。调试内核模块与调试普通应用程序有所不同,因为内核运行在特权模式,不能直接用 GDB 附加(attach)到内核进程。
GDB 调试内核的两种方式:
方式一:GDB + QEMU(最常用,适合开发阶段)
宿主机运行 GDB
QEMU 模拟目标机,提供 GDB 远程调试接口
通过 TCP 连接进行调试
方式二:GDB + KGDB(适合真实硬件)
目标机运行 KGDB(内核内置调试器)
宿主机运行 GDB,通过串口或网络连接
详见 12.2 节
12.1.2 使用 GDB + QEMU 调试内核
步骤一:编译带调试信息的内核
bash
# 配置内核,开启调试选项
make menuconfig
# → Kernel hacking
# → Compile-time checks and compiler options
# → [*] Compile the kernel with debug info (CONFIG_DEBUG_INFO=y)
# → [*] Kernel debugging (CONFIG_DEBUG_KERNEL=y)
# → [*] Magic SysRq key (CONFIG_MAGIC_SYSRQ=y)
# 关闭地址随机化(方便调试)
# → Processor type and features
# → [ ] Randomize the address of the kernel image (KASLR)
# 编译内核(保留调试符号)
make -j$(nproc)
# 验证调试信息
file vmlinux
# vmlinux: ELF 64-bit LSB executable, x86-64, ... with debug_info, not stripped
步骤二:启动 QEMU 并开启 GDB 服务
bash
# 启动 QEMU,-s 开启 GDB 服务(监听 1234 端口),-S 启动时暂停
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic \
-s \ # 等价于 -gdb tcp::1234
-S # 启动时暂停,等待 GDB 连接
# ARM 平台
qemu-system-arm \
-M vexpress-a9 \
-kernel arch/arm/boot/zImage \
-dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-initrd initramfs.img \
-nographic \
-s -S
步骤三:连接 GDB 进行调试
bash
# 在另一个终端启动 GDB
gdb vmlinux
# 连接到 QEMU 的 GDB 服务
(gdb) target remote :1234
# Remote debugging using :1234
# 0x000000000000fff0 in ?? ()
# 设置断点
(gdb) break start_kernel
# Breakpoint 1 at 0xffffffff82a5e3c0: file init/main.c, line 500.
(gdb) break drivers/char/my_driver.c:my_open
# Breakpoint 2 at 0xffffffffc0001234: file drivers/char/my_driver.c, line 45.
# 继续执行
(gdb) continue
# Continuing.
# Breakpoint 1, start_kernel () at init/main.c:500
# 查看源代码
(gdb) list
# 495 asmlinkage __visible void __init start_kernel(void)
# 496 {
# 497 char *command_line;
# 498 char *after_dashes;
# 单步执行
(gdb) next # 执行下一行(不进入函数)
(gdb) step # 执行下一行(进入函数)
(gdb) finish # 执行到当前函数返回
# 查看变量
(gdb) print jiffies
# $1 = 4294967295
(gdb) print *dev
# $2 = {cdev = {...}, mem = {...}, mutex = {...}}
# 查看寄存器
(gdb) info registers
# rax 0x0 0
# rbx 0xffffffff82a5e3c0 ...
# 查看调用栈
(gdb) backtrace
# #0 start_kernel () at init/main.c:500
# #1 0xffffffff81000107 in x86_64_start_reservations (...)
# #2 0xffffffff81000230 in x86_64_start_kernel (...)
# 查看内存
(gdb) x/16xb 0xffffffff82a5e3c0 # 以十六进制字节显示16字节
(gdb) x/4xw dev->mem # 以十六进制字显示4个字
# 修改变量值
(gdb) set variable dev->current_len = 0
# 退出 GDB
(gdb) quit
12.1.3 调试内核模块
调试内核模块需要额外步骤,因为模块是动态加载的:
bash
# 步骤1:加载模块后,获取模块加载地址
cat /sys/module/my_driver/sections/.text
# 0xffffffffc0001000
# 步骤2:在 GDB 中加载模块符号
(gdb) add-symbol-file my_driver.ko 0xffffffffc0001000
# add symbol table from file "my_driver.ko" at
# .text_addr = 0xffffffffc0001000
# (y or n) y
# 步骤3:设置断点
(gdb) break my_driver.c:my_open
# Breakpoint 3 at 0xffffffffc0001234
# 或者使用脚本自动化
# 创建 gdb_load_module.sh:
cat > gdb_load_module.sh << 'EOF'
#!/bin/bash
MODULE=$1
TEXT_ADDR=$(cat /sys/module/${MODULE}/sections/.text)
echo "add-symbol-file ${MODULE}.ko ${TEXT_ADDR}"
EOF
12.2 KGDB 调试内核
12.2.1 KGDB 的概念
KGDB(Kernel GNU Debugger)是 Linux 内核内置的调试器,允许在真实硬件上通过串口或网络进行内核级调试:
KGDB 调试架构:
宿主机(Host) 目标机(Target)
───────────────── ─────────────────────────
Ubuntu x86_64 ARM 开发板
GDB Linux 内核(含 KGDB)
↕ 串口(/dev/ttyUSB0) ↕ 串口(/dev/ttyS0)
↕ 或网络(kgdboe) ↕ 或网络
12.2.2 配置 KGDB
内核配置:
bash
make menuconfig
# → Kernel hacking
# → [*] KGDB: kernel debugger (CONFIG_KGDB=y)
# → [*] KGDB: use kgdb over the serial console (CONFIG_KGDB_SERIAL_CONSOLE=y)
# → [*] Compile the kernel with debug info (CONFIG_DEBUG_INFO=y)
# → [*] Compile the kernel with frame pointers (CONFIG_FRAME_POINTER=y)
目标机启动参数:
bash
# 在 U-Boot 中设置内核启动参数
setenv bootargs "console=ttyS0,115200 kgdboc=ttyS0,115200 kgdbwait"
# kgdboc:KGDB over console(通过串口)
# kgdbwait:启动时等待 GDB 连接
# 或者在运行时触发 KGDB
echo g > /proc/sysrq-trigger # 通过 SysRq 触发
12.2.3 使用 KGDB 调试
bash
# 宿主机:启动 GDB 并连接到目标机串口
arm-linux-gnueabihf-gdb vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyUSB0
# Remote debugging using /dev/ttyUSB0
# 0xc0008000 in ?? ()
# 后续操作与 GDB + QEMU 相同
(gdb) break my_driver.c:my_open
(gdb) continue
# 通过网络连接(kgdboe)
(gdb) target remote 192.168.1.100:6443
12.3 使用 printk 调试
12.3.1 printk 的日志级别
printk 是驱动开发中最基本、最常用的调试手段:
c
#include <linux/kernel.h>
/* 8 个日志级别(数字越小,优先级越高)*/
printk(KERN_EMERG "0: 系统不可用\n");
printk(KERN_ALERT "1: 需要立即处理\n");
printk(KERN_CRIT "2: 严重错误\n");
printk(KERN_ERR "3: 错误\n");
printk(KERN_WARNING "4: 警告\n");
printk(KERN_NOTICE "5: 普通通知\n");
printk(KERN_INFO "6: 一般信息\n");
printk(KERN_DEBUG "7: 调试信息\n");
/* 简化宏(推荐使用)*/
pr_emerg("...");
pr_alert("...");
pr_crit("...");
pr_err("错误码:%d\n", ret);
pr_warn("警告:%s\n", msg);
pr_notice("...");
pr_info("驱动版本:%s\n", VERSION);
pr_debug("调试:寄存器值 = 0x%08x\n", val);
/* 带设备信息的打印(在 platform_driver 中推荐)*/
dev_err(&pdev->dev, "初始化失败:%d\n", ret);
dev_warn(&pdev->dev, "参数超出范围\n");
dev_info(&pdev->dev, "设备 %s 注册成功\n", dev_name(&pdev->dev));
dev_dbg(&pdev->dev, "调试:%s\n", msg);
12.3.2 控制 printk 输出
bash
# 查看当前控制台日志级别
cat /proc/sys/kernel/printk
# 7 4 1 7
# ^ ^ ^ ^
# | | | └── 默认消息级别(4)
# | | └───── 最低控制台级别(1)
# | └──────── 默认控制台级别(4)
# └─────────── 当前控制台级别(7)
# 只有级别 < 当前控制台级别的消息才显示在控制台
# 显示所有级别的消息(包括 DEBUG)
echo 8 > /proc/sys/kernel/printk
# 只显示严重错误
echo 3 > /proc/sys/kernel/printk
# 查看内核日志缓冲区
dmesg
dmesg | grep "my_driver"
dmesg | tail -20
dmesg -w # 实时监控(类似 tail -f)
dmesg -c # 查看并清空缓冲区
dmesg -T # 显示人类可读的时间戳
# 设置内核日志缓冲区大小(启动参数)
# log_buf_len=16M ← 设置为 16MB
12.3.3 动态调试(Dynamic Debug)
动态调试允许在运行时按需开启/关闭特定的调试输出,无需重新编译内核:
bash
# 挂载 debugfs(通常已自动挂载)
mount -t debugfs none /sys/kernel/debug
# 查看动态调试控制文件
cat /sys/kernel/debug/dynamic_debug/control | head -20
# drivers/char/my_driver.c:45 [my_driver]my_open =_ "设备打开\n"
# drivers/char/my_driver.c:67 [my_driver]my_read =_ "读取 %d 字节\n"
# 开启特定文件的所有调试输出
echo "file my_driver.c +p" > /sys/kernel/debug/dynamic_debug/control
# 开启特定函数的调试输出
echo "func my_open +p" > /sys/kernel/debug/dynamic_debug/control
# 开启特定模块的所有调试输出
echo "module my_driver +p" > /sys/kernel/debug/dynamic_debug/control
# 关闭调试输出
echo "file my_driver.c -p" > /sys/kernel/debug/dynamic_debug/control
# 开启时显示文件名、行号、函数名
echo "module my_driver +pflm" > /sys/kernel/debug/dynamic_debug/control
# p:打印
# f:显示函数名
# l:显示行号
# m:显示模块名
# t:显示线程 ID
在驱动中使用动态调试:
c
/* pr_debug 和 dev_dbg 支持动态调试 */
/* 需要在编译时定义 DEBUG 宏,或使用动态调试控制 */
/* 方法一:在源文件开头定义 DEBUG */
#define DEBUG
#include <linux/kernel.h>
pr_debug("这条消息只在 DEBUG 模式下输出\n");
/* 方法二:在 Makefile 中定义 */
/* ccflags-y += -DDEBUG */
/* 方法三:使用动态调试(推荐)*/
/* 无需修改代码,运行时通过 debugfs 控制 */
dev_dbg(&pdev->dev, "动态调试消息:val = %d\n", val);
12.3.4 printk 的高级用法
c
/* 打印十六进制数据(调试数据包、寄存器等)*/
#include <linux/printk.h>
u8 data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
/* print_hex_dump:打印十六进制转储 */
print_hex_dump(KERN_DEBUG, /* 日志级别 */
"data: ", /* 前缀 */
DUMP_PREFIX_OFFSET, /* 前缀类型(偏移量)*/
16, /* 每行字节数 */
1, /* 每组字节数 */
data, /* 数据指针 */
sizeof(data), /* 数据长度 */
true); /* 是否打印 ASCII */
/* 输出:
* data: 00000000: 01 02 03 04 05 06 07 08 ........
*/
/* 打印调用栈(调试崩溃位置)*/
dump_stack();
/* 打印当前进程信息 */
pr_info("当前进程:%s(PID=%d)\n", current->comm, current->pid);
/* 限速打印(防止日志刷屏)*/
printk_ratelimited(KERN_WARNING "警告:设备错误(每秒最多打印一次)\n");
pr_warn_ratelimited("限速警告\n");
12.4 使用 /proc 调试
12.4.1 /proc 文件系统简介
/proc 是一个虚拟文件系统 ,提供内核和进程信息的接口。驱动可以在 /proc 下创建文件,向用户空间暴露调试信息:
bash
# 常用的 /proc 调试文件
cat /proc/interrupts # 中断统计
cat /proc/iomem # I/O 内存映射
cat /proc/ioports # I/O 端口映射
cat /proc/modules # 已加载模块
cat /proc/meminfo # 内存信息
cat /proc/slabinfo # slab 缓存信息
cat /proc/devices # 设备号分配
cat /proc/kmsg # 内核消息(类似 dmesg)
cat /proc/sys/kernel/printk # printk 日志级别
12.4.2 在驱动中创建 /proc 文件
c
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
static struct proc_dir_entry *proc_entry;
/* seq_file 接口(推荐,适合输出多行信息)*/
/* show 函数:输出调试信息 */
static int my_proc_show(struct seq_file *m, void *v)
{
struct my_dev *dev = m->private;
seq_printf(m, "=== My Driver Debug Info ===\n");
seq_printf(m, "设备名称:%s\n", DEVICE_NAME);
seq_printf(m, "主设备号:%d\n", MAJOR(dev->devno));
seq_printf(m, "当前数据长度:%u\n", dev->current_len);
seq_printf(m, "中断计数:%u\n", dev->irq_count);
seq_printf(m, "读操作次数:%lu\n", dev->read_count);
seq_printf(m, "写操作次数:%lu\n", dev->write_count);
seq_printf(m, "错误计数:%u\n", dev->error_count);
/* 打印寄存器状态 */
seq_printf(m, "\n=== 寄存器状态 ===\n");
seq_printf(m, "CTRL_REG:0x%08x\n", readl(dev->base + CTRL_REG));
seq_printf(m, "STATUS_REG:0x%08x\n", readl(dev->base + STATUS_REG));
return 0;
}
/* open 函数 */
static int my_proc_open(struct inode *inode, struct file *file)
{
return single_open(file, my_proc_show, PDE_DATA(inode));
}
/* file_operations */
static const struct file_operations my_proc_fops = {
.owner = THIS_MODULE,
.open = my_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
/* 在 probe 函数中创建 /proc 文件 */
static int my_probe(struct platform_device *pdev)
{
/* ... 其他初始化 ... */
/* 创建 /proc/my_driver 文件 */
proc_entry = proc_create_data("my_driver",
0444, /* 只读权限 */
NULL, /* 父目录(NULL = /proc)*/
&my_proc_fops,
dev); /* 传给 show 函数的数据 */
if (!proc_entry) {
dev_err(&pdev->dev, "创建 /proc/my_driver 失败\n");
return -ENOMEM;
}
return 0;
}
/* 在 remove 函数中删除 /proc 文件 */
static int my_remove(struct platform_device *pdev)
{
proc_remove(proc_entry);
return 0;
}
用户空间查看 /proc 调试信息:
bash
# 查看驱动调试信息
cat /proc/my_driver
# === My Driver Debug Info ===
# 设备名称:my_device
# 主设备号:230
# 当前数据长度:128
# 中断计数:1024
# 读操作次数:512
# 写操作次数:256
# 错误计数:0
#
# === 寄存器状态 ===
# CTRL_REG:0x00000001
# STATUS_REG:0x00000003
12.4.3 创建可写的 /proc 文件
c
/* 支持读写的 /proc 文件(用于运行时修改驱动参数)*/
static ssize_t my_proc_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
char kbuf[64];
int val;
if (count >= sizeof(kbuf))
return -EINVAL;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
kbuf[count] = '\0';
if (kstrtoint(kbuf, 10, &val))
return -EINVAL;
/* 修改驱动参数 */
debug_level = val;
pr_info("调试级别已设置为 %d\n", debug_level);
return count;
}
static const struct file_operations my_proc_rw_fops = {
.owner = THIS_MODULE,
.open = my_proc_open,
.read = seq_read,
.write = my_proc_write, /* 支持写操作 */
.llseek = seq_lseek,
.release = single_release,
};
/* 创建可读写的 /proc 文件 */
proc_create("my_driver_ctrl", 0644, NULL, &my_proc_rw_fops);
/* 用户空间使用 */
/* echo 2 > /proc/my_driver_ctrl ← 设置调试级别为2 */
/* cat /proc/my_driver_ctrl ← 查看当前调试级别 */
12.5 使用 Oops 调试
12.5.1 什么是 Oops
Oops 是 Linux 内核在检测到严重错误时(如空指针解引用、访问无效内存)打印的错误报告。分析 Oops 信息是调试内核崩溃的重要手段:
常见导致 Oops 的原因:
1. 空指针解引用(NULL pointer dereference)
2. 访问已释放的内存(use-after-free)
3. 栈溢出(stack overflow)
4. 除以零(division by zero)
5. 访问用户空间地址(未使用 copy_to/from_user)
12.5.2 Oops 信息解读
制造一个 Oops(示例):
c
/* 故意制造空指针解引用 */
static int my_open(struct inode *inode, struct file *filp)
{
struct my_dev *dev = NULL; /* 故意设为 NULL */
dev->count++; /* 解引用 NULL 指针 → Oops!*/
return 0;
}
Oops 输出示例:
BUG: unable to handle kernel NULL pointer dereference at 0000000000000008
IP: [<ffffffffc0001234>] my_open+0x24/0x80 [my_driver]
PGD 0
Oops: 0002 [#1] SMP
Modules linked in: my_driver(O)
CPU: 0 PID: 1234 Comm: cat Tainted: G O 4.0.0 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996)
task: ffff880007a80000 ti: ffff880007a84000 task.ti: ffff880007a84000
RIP: 0010:[<ffffffffc0001234>] [<ffffffffc0001234>] my_open+0x24/0x80 [my_driver]
RSP: 0018:ffff880007a87e08 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff880007b12800 RCX: 0000000000000000
RDX: 0000000000000000 RSI: ffff880007b12800 RDI: ffff880007b12900
RBP: ffff880007a87e28 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffff880007b12900
R13: ffff880007b12800 R14: 0000000000000000 R15: 0000000000000000
FS: 00007f1234567890(0000) GS:ffff88000fc00000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000000008 CR3: 0000000007a0e000 CR4: 00000000000006f0
Stack:
ffff880007b12900 ffff880007b12800 ffff880007a87e48 ffffffff811234ab
...
Call Trace:
[<ffffffff811234ab>] do_dentry_open+0x1ab/0x2c0
[<ffffffff81123abc>] vfs_open+0x4c/0x70
[<ffffffff81124def>] do_last+0x3ef/0x1100
[<ffffffff81125678>] path_openat+0x78/0x6c0
[<ffffffff81126789>] do_filp_open+0x89/0xf0
[<ffffffff8112789a>] do_sys_open+0x12a/0x200
[<ffffffff811289ab>] SyS_open+0x1b/0x20
[<ffffffff8100012b>] system_call_fastpath+0x12/0x17
Code: 48 89 df e8 xx xx xx xx 48 85 c0 74 xx 48 8b 40 08 ...
RIP [<ffffffffc0001234>] my_open+0x24/0x80 [my_driver]
RSP <ffff880007a87e08>
CR2: 0000000000000008
12.5.3 Oops 信息分析方法
Oops 关键信息解读:
1. 错误类型
"unable to handle kernel NULL pointer dereference at 0000000000000008"
→ 访问地址 0x8(NULL + 偏移8),说明对 NULL 指针的成员进行了访问
2. 出错位置
"IP: [<ffffffffc0001234>] my_open+0x24/0x80 [my_driver]"
→ 出错在 my_driver 模块的 my_open 函数,偏移 0x24,函数总大小 0x80
3. Oops 代码
"Oops: 0002 [#1] SMP"
→ 0002:写操作导致的页错误(0000=读,0002=写,0001=用户空间)
→ [#1]:第1次 Oops
→ SMP:多处理器系统
4. 调用栈(Call Trace)
→ 从下往上读,显示函数调用链
→ my_open ← do_dentry_open ← vfs_open ← ... ← SyS_open
5. CR2 寄存器
"CR2: 0000000000000008"
→ 导致页错误的虚拟地址(0x8 = NULL + 8)
使用 addr2line 定位出错代码行:
bash
# 方法一:addr2line(需要调试符号)
addr2line -e my_driver.ko -i 0x24
# my_driver.c:47
# 方法二:objdump 反汇编
objdump -d my_driver.ko | grep -A 20 "<my_open>"
# 0000000000000000 <my_open>:
# 0: 55 push %rbp
# ...
# 24: 48 8b 40 08 mov 0x8(%rax),%rax ← 出错位置(访问 NULL+8)
# 方法三:gdb 分析
gdb my_driver.ko
(gdb) list *(my_open+0x24)
# my_driver.c:47: dev->count++;
# 方法四:使用 scripts/decode_stacktrace.sh(内核提供的工具)
dmesg | ./scripts/decode_stacktrace.sh vmlinux
12.5.4 常见 Oops 类型及解决方法
常见 Oops 类型:
1. NULL pointer dereference(最常见)
原因:使用了未初始化或已释放的指针
解决:检查指针是否为 NULL,使用前验证
2. general protection fault
原因:访问了无效的内存地址
解决:检查内存分配是否成功,边界是否正确
3. stack overflow
原因:内核栈(通常 8KB)溢出
解决:减少栈上的大数组,改用 kmalloc
4. BUG: scheduling while atomic
原因:在不可睡眠的上下文中调用了可能睡眠的函数
解决:检查是否在中断上下文或持有自旋锁时调用了 msleep 等
5. WARNING: CPU: X PID: Y at ...
原因:触发了内核的 WARN_ON 检查
解决:分析 WARN_ON 的条件,修复相应的逻辑错误
12.6 使用 strace 调试
12.6.1 strace 的概念
strace 是用户空间工具,用于跟踪进程的系统调用,帮助分析应用程序与驱动之间的交互:
bash
# 基本用法:跟踪程序的所有系统调用
strace ./my_app
# 跟踪特定系统调用
strace -e trace=open,read,write,ioctl ./my_app
# 跟踪已运行的进程
strace -p 1234
# 显示时间戳
strace -t ./my_app
# 显示每个系统调用的耗时
strace -T ./my_app
# 统计系统调用次数和耗时
strace -c ./my_app
# 将输出保存到文件
strace -o strace.log ./my_app
# 跟踪子进程
strace -f ./my_app
12.6.2 strace 调试驱动的案例
bash
# 案例:调试 globalfifo 驱动的读写操作
strace -e trace=open,read,write,ioctl,close ./test_globalfifo
# 输出示例:
# open("/dev/globalfifo", O_RDWR) = 3
# write(3, "Hello, Driver!", 14) = 14
# lseek(3, 0, SEEK_SET) = 0
# read(3, "Hello, Driver!", 14) = 14
# ioctl(3, 0x1, 0) = 0 ← MEM_CLEAR 命令
# close(3) = 0
# 案例:调试驱动返回错误的原因
strace -e trace=open,ioctl ./my_app 2>&1 | grep -E "ENODEV|EINVAL|EBUSY"
# ioctl(3, SOME_CMD, 0x7fff...) = -1 EINVAL (Invalid argument)
# → 说明 ioctl 命令参数无效,检查驱动的 ioctl 实现
# 案例:分析驱动阻塞的原因
strace -T ./blocking_app
# read(3, <unfinished ...> ← 阻塞在 read
# ...(等待很长时间)
# <... read resumed> "data", 4) = 4 ← 最终返回
# → 说明 read 阻塞了,驱动的等待队列工作正常
12.6.3 ltrace 跟踪库函数调用
bash
# ltrace:跟踪库函数调用(与 strace 互补)
ltrace ./my_app
# 同时跟踪系统调用和库函数
ltrace -S ./my_app
12.7 使用 ioctl 调试
12.7.1 ioctl 调试接口的设计
在驱动中设计专用的 ioctl 调试命令,允许用户空间动态查询和修改驱动状态:
c
/* 调试 ioctl 命令定义 */
#define MY_DRIVER_MAGIC 'd'
/* 获取驱动统计信息 */
#define IOCTL_GET_STATS _IOR(MY_DRIVER_MAGIC, 0, struct driver_stats)
/* 设置调试级别 */
#define IOCTL_SET_DEBUG _IOW(MY_DRIVER_MAGIC, 1, int)
/* 获取寄存器值 */
#define IOCTL_GET_REG _IOWR(MY_DRIVER_MAGIC, 2, struct reg_access)
/* 设置寄存器值 */
#define IOCTL_SET_REG _IOW(MY_DRIVER_MAGIC, 3, struct reg_access)
/* 重置驱动统计 */
#define IOCTL_RESET_STATS _IO(MY_DRIVER_MAGIC, 4)
/* 触发自检 */
#define IOCTL_SELF_TEST _IO(MY_DRIVER_MAGIC, 5)
/* 统计信息结构体 */
struct driver_stats {
unsigned long read_count;
unsigned long write_count;
unsigned long irq_count;
unsigned long error_count;
unsigned int current_len;
unsigned int debug_level;
};
/* 寄存器访问结构体 */
struct reg_access {
unsigned int offset; /* 寄存器偏移 */
unsigned int value; /* 读取/写入的值 */
};
驱动中实现调试 ioctl:
c
static long my_debug_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct my_dev *dev = filp->private_data;
struct driver_stats stats;
struct reg_access reg;
int debug_val;
/* 验证命令类型 */
if (_IOC_TYPE(cmd) != MY_DRIVER_MAGIC)
return -ENOTTY;
switch (cmd) {
case IOCTL_GET_STATS:
/* 收集统计信息 */
stats.read_count = dev->read_count;
stats.write_count = dev->write_count;
stats.irq_count = dev->irq_count;
stats.error_count = dev->error_count;
stats.current_len = dev->current_len;
stats.debug_level = debug_level;
if (copy_to_user((struct driver_stats __user *)arg,
&stats, sizeof(stats)))
return -EFAULT;
break;
case IOCTL_SET_DEBUG:
if (copy_from_user(&debug_val, (int __user *)arg, sizeof(int)))
return -EFAULT;
if (debug_val < 0 || debug_val > 7)
return -EINVAL;
debug_level = debug_val;
pr_info("调试级别设置为 %d\n", debug_level);
break;
case IOCTL_GET_REG:
if (copy_from_user(®, (struct reg_access __user *)arg,
sizeof(reg)))
return -EFAULT;
if (reg.offset >= dev->reg_size)
return -EINVAL;
reg.value = readl(dev->base + reg.offset);
if (copy_to_user((struct reg_access __user *)arg,
®, sizeof(reg)))
return -EFAULT;
break;
case IOCTL_SET_REG:
if (copy_from_user(®, (struct reg_access __user *)arg,
sizeof(reg)))
return -EFAULT;
if (reg.offset >= dev->reg_size)
return -EINVAL;
writel(reg.value, dev->base + reg.offset);
pr_info("寄存器 0x%x 设置为 0x%x\n", reg.offset, reg.value);
break;
case IOCTL_RESET_STATS:
dev->read_count = 0;
dev->write_count = 0;
dev->irq_count = 0;
dev->error_count = 0;
pr_info("统计信息已重置\n");
break;
case IOCTL_SELF_TEST:
return my_driver_self_test(dev);
default:
return -EINVAL;
}
return 0;
}
用户空间调试工具:
c
/*
* my_driver_debug.c ------ 驱动调试工具
* 通过 ioctl 查询和控制驱动状态
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
/* 包含驱动头文件(定义 ioctl 命令)*/
#include "my_driver.h"
int main(int argc, char *argv[])
{
int fd = open("/dev/my_device", O_RDWR);
if (fd < 0) { perror("open"); return -1; }
if (argc < 2) {
printf("用法:%s <命令>\n", argv[0]);
printf(" stats - 显示统计信息\n");
printf(" debug N - 设置调试级别\n");
printf(" reg R - 读取寄存器\n");
printf(" reset - 重置统计\n");
printf(" test - 自检\n");
close(fd);
return 0;
}
if (strcmp(argv[1], "stats") == 0) {
struct driver_stats stats;
ioctl(fd, IOCTL_GET_STATS, &stats);
printf("读操作:%lu\n", stats.read_count);
printf("写操作:%lu\n", stats.write_count);
printf("中断:%lu\n", stats.irq_count);
printf("错误:%lu\n", stats.error_count);
printf("当前数据长度:%u\n", stats.current_len);
printf("调试级别:%u\n", stats.debug_level);
} else if (strcmp(argv[1], "debug") == 0 && argc >= 3) {
int level = atoi(argv[2]);
ioctl(fd, IOCTL_SET_DEBUG, &level);
printf("调试级别已设置为 %d\n", level);
} else if (strcmp(argv[1], "reg") == 0 && argc >= 3) {
struct reg_access reg;
reg.offset = strtoul(argv[2], NULL, 16);
ioctl(fd, IOCTL_GET_REG, ®);
printf("寄存器 0x%x = 0x%08x\n", reg.offset, reg.value);
} else if (strcmp(argv[1], "reset") == 0) {
ioctl(fd, IOCTL_RESET_STATS);
printf("统计信息已重置\n");
} else if (strcmp(argv[1], "test") == 0) {
int ret = ioctl(fd, IOCTL_SELF_TEST);
printf("自检结果:%s\n", ret == 0 ? "通过" : "失败");
}
close(fd);
return 0;
}
12.8 性能调试工具
12.8.1 perf ------ Linux 性能分析工具
perf 是 Linux 内核自带的性能分析工具,可以分析 CPU 使用、缓存命中率、函数调用热点等:
bash
# 安装 perf
sudo apt-get install linux-tools-$(uname -r)
# ── 基本性能统计 ──────────────────────────────────────────
# 统计程序运行期间的性能事件
perf stat ./my_app
# Performance counter stats for './my_app':
#
# 1,234.56 msec task-clock # 0.987 CPUs utilized
# 5 context-switches # 0.004 K/sec
# 0 cpu-migrations # 0.000 K/sec
# 123 page-faults # 0.100 K/sec
# 3,456,789,012 cycles # 2.800 GHz
# 2,345,678,901 instructions # 0.68 insn per cycle
# 456,789,012 branches # 369.999 M/sec
# 1,234,567 branch-misses # 0.27% of all branches
# ── CPU 热点分析 ──────────────────────────────────────────
# 采样分析(找出 CPU 使用最多的函数)
perf record -g ./my_app # 记录(-g 记录调用栈)
perf report # 分析报告
# 实时显示 CPU 热点
perf top
perf top -p 1234 # 只分析指定进程
# ── 内核函数分析 ──────────────────────────────────────────
# 分析内核函数调用
sudo perf record -g -a sleep 10 # 记录10秒内所有CPU的事件
sudo perf report --stdio
# ── 特定事件分析 ──────────────────────────────────────────
# 分析 Cache 缺失
perf stat -e cache-misses,cache-references ./my_app
# 分析页错误
perf stat -e page-faults ./my_app
# 分析上下文切换
perf stat -e context-switches ./my_app
# ── 火焰图(Flame Graph)──────────────────────────────────
# 生成火焰图(需要 FlameGraph 工具)
perf record -g ./my_app
perf script | ./FlameGraph/stackcollapse-perf.pl | \
./FlameGraph/flamegraph.pl > flamegraph.svg
12.8.2 ftrace ------ 内核函数跟踪
ftrace 是 Linux 内核内置的跟踪框架,可以跟踪内核函数调用:
bash
# ftrace 通过 debugfs 控制
cd /sys/kernel/debug/tracing
# 查看可用的跟踪器
cat available_tracers
# blk function_graph wakeup_dl wakeup_rt wakeup function nop
# ── 函数跟踪器 ────────────────────────────────────────────
# 设置跟踪器为函数跟踪
echo function > current_tracer
# 设置要跟踪的函数(支持通配符)
echo "my_driver_*" > set_ftrace_filter
echo "globalfifo_*" >> set_ftrace_filter
# 开始跟踪
echo 1 > tracing_on
# 执行要调试的操作
cat /dev/globalfifo
# 停止跟踪
echo 0 > tracing_on
# 查看跟踪结果
cat trace
# # tracer: function
# #
# # TASK-PID CPU# |||| TIMESTAMP FUNCTION
# # | | | |||| | |
# cat-1234 [000] .... 1234.567890: globalfifo_open <-do_dentry_open
# cat-1234 [000] .... 1234.567891: globalfifo_read <-vfs_read
# cat-1234 [000] .... 1234.567892: globalfifo_release <-__fput
# ── 函数图跟踪器(显示调用关系和耗时)────────────────────
echo function_graph > current_tracer
echo "globalfifo_read" > set_graph_function
echo 1 > tracing_on
cat /dev/globalfifo
echo 0 > tracing_on
cat trace
# # tracer: function_graph
# #
# # CPU DURATION FUNCTION CALLS
# # | | | | | | |
# 0) | globalfifo_read() {
# 0) 0.123 us | mutex_lock();
# 0) 0.456 us | copy_to_user();
# 0) 0.789 us | mutex_unlock();
# 0) 1.368 us | }
# ── 事件跟踪 ──────────────────────────────────────────────
# 跟踪特定内核事件
ls available_events | grep irq
# irq:irq_handler_entry
# irq:irq_handler_exit
echo irq:irq_handler_entry > set_event
echo 1 > tracing_on
# ... 触发中断 ...
echo 0 > tracing_on
cat trace
# 清理
echo nop > current_tracer
echo > set_ftrace_filter
echo > trace
12.8.3 valgrind ------ 内存错误检测
valgrind 用于检测用户空间程序的内存错误(内核驱动不能直接使用,但可以用于测试用户空间测试程序):
bash
# 检测内存泄漏和错误
valgrind --leak-check=full ./test_my_driver
# 输出示例:
# ==1234== Memcheck, a memory error detector
# ==1234== LEAK SUMMARY:
# ==1234== definitely lost: 1,024 bytes in 1 blocks
# ==1234== indirectly lost: 0 bytes in 0 blocks
# ==1234== possibly lost: 0 bytes in 0 blocks
# ==1234== still reachable: 0 bytes in 0 blocks
# ==1234== suppressed: 0 bytes in 0 blocks
12.8.4 kmemleak ------ 内核内存泄漏检测
kmemleak 是内核内置的内存泄漏检测工具:
bash
# 内核配置
# CONFIG_DEBUG_KMEMLEAK=y
# 使用 kmemleak
mount -t debugfs none /sys/kernel/debug
# 触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# unreferenced object 0xffff880007a80000 (size 1024):
# comm "insmod", pid 1234, jiffies 4294967295
# backtrace:
# [<ffffffff811234ab>] kmalloc+0x1b/0x30
# [<ffffffffc0001234>] my_driver_init+0x34/0x80 [my_driver]
# ...
# 清除已知泄漏
echo clear > /sys/kernel/debug/kmemleak
12.8.5 lockdep ------ 死锁检测
lockdep 是内核内置的锁依赖检测工具,可以在运行时检测潜在的死锁:
bash
# 内核配置
# CONFIG_PROVE_LOCKING=y
# CONFIG_DEBUG_LOCKDEP=y
# lockdep 自动运行,检测到问题时打印警告
# 示例输出:
# ======================================================
# WARNING: possible circular locking dependency detected
# 4.0.0 #1 Not tainted
# ------------------------------------------------------
# my_app/1234 is trying to acquire lock:
# (&dev->mutex){+.+.+.}, at: [<ffffffffc0001234>] my_read+0x24/0x80
#
# but task is already holding lock:
# (&dev->spinlock){+.+.+.}, at: [<ffffffffc0001567>] my_irq+0x17/0x40
#
# which lock already depends on the new lock.
12.8.6 调试工具总结
Linux 驱动调试工具汇总:
工具 类型 适用场景
─────────────────────────────────────────────────────────────
GDB + QEMU 交互调试 开发阶段,虚拟机调试
KGDB 交互调试 真实硬件,串口/网络调试
printk/pr_debug 日志输出 最基本,随时可用
动态调试 日志输出 运行时按需开启,无需重编译
/proc 接口 状态查询 暴露驱动内部状态
Oops 分析 崩溃分析 内核崩溃后的事后分析
strace 系统调用 分析用户空间与驱动的交互
ioctl 调试接口 运行时控制 动态查询/修改驱动状态
perf 性能分析 CPU 热点、Cache 命中率
ftrace 函数跟踪 内核函数调用链分析
kmemleak 内存泄漏 检测内核内存泄漏
lockdep 死锁检测 检测潜在死锁
valgrind 内存检测 用户空间测试程序的内存错误
本章小结
| 章节 | 核心知识点 | 关键工具/命令 |
|---|---|---|
| 12.1 GDB调试内核 | GDB+QEMU调试流程;编译调试内核;断点/单步/查看变量;调试内核模块(add-symbol-file) | gdb vmlinux、target remote :1234、add-symbol-file |
| 12.2 KGDB调试内核 | KGDB架构;内核配置;kgdboc/kgdbwait参数;串口连接 | kgdboc=ttyS0、target remote /dev/ttyUSB0 |
| 12.3 printk调试 | 8个日志级别;pr_xxx简化宏;dev_xxx设备宏;动态调试(+p标志);print_hex_dump;限速打印 | dmesg、/sys/kernel/debug/dynamic_debug/control |
| 12.4 /proc调试 | seq_file接口;proc_create_data;可读写/proc文件;运行时修改参数 | cat /proc/my_driver、proc_create() |
| 12.5 Oops调试 | Oops信息结构解读;CR2寄存器;Call Trace分析;addr2line定位代码行;常见Oops类型 | addr2line、objdump -d、decode_stacktrace.sh |
| 12.6 strace调试 | 系统调用跟踪;-e过滤;-T耗时;-c统计;驱动交互分析案例 | strace -e trace=open,read,write,ioctl |
| 12.7 ioctl调试 | 调试ioctl命令设计;统计信息/寄存器读写/自检;完整调试工具程序 | IOCTL_GET_STATS、IOCTL_SET_DEBUG |
| 12.8 性能调试工具 | perf stat/record/report/top;ftrace函数跟踪/函数图;kmemleak内存泄漏;lockdep死锁检测 | perf top、ftrace、kmemleak、lockdep |
调试方法选择指南
遇到问题时的调试策略:
驱动加载失败 → printk + dmesg 查看错误信息
功能不正确 → printk 跟踪执行流程 + strace 分析系统调用
内核崩溃 → 分析 Oops 信息 + addr2line 定位代码行
性能问题 → perf 分析热点 + ftrace 跟踪函数调用
内存泄漏 → kmemleak 扫描
死锁问题 → lockdep 检测
复杂逻辑 → GDB + QEMU 单步调试
运行时状态 → /proc 接口 + ioctl 调试命令
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年