《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 12 章 Linux 设备驱动的调试

《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(&reg, (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,
                          &reg, sizeof(reg)))
            return -EFAULT;
        break;

    case IOCTL_SET_REG:
        if (copy_from_user(&reg, (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, &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 vmlinuxtarget remote :1234add-symbol-file
12.2 KGDB调试内核 KGDB架构;内核配置;kgdboc/kgdbwait参数;串口连接 kgdboc=ttyS0target 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_driverproc_create()
12.5 Oops调试 Oops信息结构解读;CR2寄存器;Call Trace分析;addr2line定位代码行;常见Oops类型 addr2lineobjdump -ddecode_stacktrace.sh
12.6 strace调试 系统调用跟踪;-e过滤;-T耗时;-c统计;驱动交互分析案例 strace -e trace=open,read,write,ioctl
12.7 ioctl调试 调试ioctl命令设计;统计信息/寄存器读写/自检;完整调试工具程序 IOCTL_GET_STATSIOCTL_SET_DEBUG
12.8 性能调试工具 perf stat/record/report/top;ftrace函数跟踪/函数图;kmemleak内存泄漏;lockdep死锁检测 perf topftracekmemleaklockdep

调试方法选择指南

复制代码
遇到问题时的调试策略:

驱动加载失败 → printk + dmesg 查看错误信息
功能不正确   → printk 跟踪执行流程 + strace 分析系统调用
内核崩溃     → 分析 Oops 信息 + addr2line 定位代码行
性能问题     → perf 分析热点 + ftrace 跟踪函数调用
内存泄漏     → kmemleak 扫描
死锁问题     → lockdep 检测
复杂逻辑     → GDB + QEMU 单步调试
运行时状态   → /proc 接口 + ioctl 调试命令

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年