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

内核调试技术:从printk到KGDB的完整武器库

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

内核调试技术:从printk到KGDB的完整武器库

驱动开发最耗时的不是写代码,是调bug。内核态没有gdb直接挂,没有printf随便打,一个空指针oops就能让你对着Call Trace发呆半小时。本文基于Linux 6.8内核,把printk、动态调试、ftrace、kprobes、KGDB、oops分析、内存调试这些工具串一遍,每个都给操作步骤和代码示例,最后一张对比表帮你按场景选工具。

一、printk调试

printk是内核调试的瑞士军刀,上手零门槛,但用好了也有讲究。

日志级别

内核定义7个日志级别,数值越小优先级越高。
| 级别 | 值 | | 用途 |
|:-----|:---|:---|:-----|
| KERN_EMERG | pr_emerg | 0 | 系统不可用,panic前最后一句话 |
| KERN_ALERT | pr_alert | 1 | 必须立即处理 |
| KERN_CRIT | pr_crit | 2 | 严重条件 |
| KERN_ERR | pr_err | 3 | 错误条件 |
| KERN_WARNING | pr_warn | 4 | 警告 |
| KERN_NOTICE | pr_notice | 5 | 正常但值得注意 |
| KERN_INFO | pr_info | 6 | 信息 |
| KERN_DEBUG | pr_debug | 7 | 调试信息 |

控制台只显示优先级数值低于console_loglevel的消息。默认console_loglevel。(KERN_WARNING),所以pr_info和pr_debug默认不会打印到控制台,但dmesg能看到。

便利。

/* 推荐用便利宏,比printk(KERN_XXX ...)简*/
pr_emerg("system on fire!\n");
pr_alert("immediate action required\n");
pr_crit("critical condition\n");
pr_err("something went wrong: %d\n", ret);
pr_warn("suspicious value: %u\n", val);
pr_notice("event occurred\n");
pr_info("initialized successfully\n");
pr_debug("buf=%p len=%zu\n", buf, len);

pr_debug比较特殊——没开CONFIG_DYNAMIC_DEBUG时它等价于printk(KERN_DEBUG ...),开了之后走动态调试子系统,可以运行时开关。

pr_fmt格式化前缀

一堆驱动的printk输出混在一起,分不清谁打的。pr_fmt给所有printk加统一前缀。

/* 在文件最开头定义,注意在include之前 */
#define pr_fmt(fmt) "%s: " fmt, __func__

#include <linux/module.h>
#include <linux/init.h>

static int __init my_init(void)
{
    pr_info("module loaded\n");
    /* 输出:my_init: module loaded */
    return 0;
}

__func__自动填当前函数名。也可以用模块名。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/* 输出:my_module: module loaded */

dmesg查看

# 查看全部内核日志
dmesg

# 实时跟踪(类似tail -f。dmesg -w

# 按级别过滤:只看err和warn
dmesg -l err,warn

# 按级别过滤:只看debug级别
dmesg -l debug

# 按设施过。dmesg -f daemon,kern

# 时间戳转人类可读
dmesg -T

# 清空日志缓冲。sudo dmesg -c

loglevel内核参数

# 查看当前控制台日志级别cat /proc/sys/kernel/printk
# 输出四个数:当前日志级别 默认日志级别 最低日志级别启动时日志级别# 例如。  4  1  7

# 临时修改(重启失效)
echo 8 > /proc/sys/kernel/printk
# 8表示所有级别的printk都输出到控制。
# 启动参数永久修改
# 在GRUB的linux行添加:
loglevel=7

# 调试时常用:让所有printk都打到控制台
loglevel=8

开发阶段建议把loglevel设到8,所有printk直接在控制台可见,省得切终端看dmesg。

二、动态调试(Dynamic Debug。

printk的问题是:调试时加一堆pr_debug,发版时得删掉或注释掉,下次出bug又得加回来。动态调试解决了这个问题——pr_debug/dev_dbg留在代码里不动,运行时决定开不开销

基本操作

# 开启动态调试需要内核配置# CONFIG_DYNAMIC_DEBUG=y

# 开启某个文件的所有pr_debug
echo 'file drivers/i2c/i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control

# 开启某个模块的所有pr_debug
echo 'module my_driver +p' > /sys/kernel/debug/dynamic_debug/control

# 开启某个函数的pr_debug
echo 'func my_probe +p' > /sys/kernel/debug/dynamic_debug/control

# 开启某一。echo 'file my_drv.c line 42 +p' > /sys/kernel/debug/dynamic_debug/control

# 按级别过。echo 'module my_driver level=7 +p' > /sys/kernel/debug/dynamic_debug/control

# 关闭:把+p改成-p
echo 'file drivers/i2c/i2c-core.c -p' > /sys/kernel/debug/dynamic_debug/control

过滤条件组合

# 同时匹配多个条件(AND关系。echo 'file my_drv.c func my_probe +p' > /sys/kernel/debug/dynamic_debug/control

# 用OR关系:写多条命令
echo 'file my_drv.c +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file my_helper.c +p' > /sys/kernel/debug/dynamic_debug/control

# 用通配置echo 'file drivers/net/ethernet/intel/* +p' > /sys/kernel/debug/dynamic_debug/control

查看已启用的调试语句

# 查看所有已启用的pr_debug
cat /sys/kernel/debug/dynamic_debug/control | grep "=p"

# 查看某个文件的调试状态cat /sys/kernel/debug/dynamic_debug/control | grep my_drv.c

# 输出格式。# my_drv.c:42 [my_driver]my_probe =p "probe called for device %s\n"
#   文件:行号 [模块]函数。状态格式字符号```

### 启动时开销
```bash
# 内核启动参数,在GRUB中添。dyndbg="file drivers/i2c/* +p"

# 模块加载时开销modprobe my_driver dyndbg="+p"

动态调试的开销极低——关闭时pr_debug只是一个条件跳转,几乎为零。所以放心在代码里撒pr_debug,不用的时候关掉就行。

三、ftrace跟踪

ftrace是Linux内核内置的跟踪框架,不用装任何工具,挂载debugfs就能用。它能跟踪函数调用、记录调用图、统计执行时间,是性能分析和调用链梳理的利器。

function tracer:跟踪函数调试

# 确认debugfs已挂。mount -t debugfs none /sys/kernel/debug

# 查看可用的tracer
cat /sys/kernel/debug/tracing/available_tracers
# 输出:function function_graph blk mmiotrace wakeup_rt wakeup irqsoff ...

# 选择function tracer
echo function > /sys/kernel/debug/tracing/current_tracer

# 可选:只跟踪特定函数echo my_probe > /sys/kernel/debug/tracing/set_ftrace_filter

# 可选:排除特定函数
echo schedule > /sys/kernel/debug/tracing/set_ftrace_notrace

# 开启跟。echo 1 > /sys/kernel/debug/tracing/tracing_on

# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace

# 关闭跟踪
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 清空缓冲。echo > /sys/kernel/debug/tracing/trace

trace输出示例。

# tracer: function
#
#           TASK-PID   CPU#  TIMESTAMP  FUNCTION
#             | |      |      |        |
           <...>-1234  [000]  1234.567890: my_probe <-pci_device_probe
           <...>-1234  [000]  1234.567891: my_init_hw <-my_probe
           <...>-1234  [000]  1234.567892: my_configure <-my_init_hw

function_graph tracer:调用图

function_graph比function更直观——它展示函数的调用层级和返回关系。

# 切换到function_graph tracer
echo function_graph > /sys/kernel/debug/tracing/current_tracer

# 限制跟踪范围(强烈建议,否则输出爆炸。echo my_probe > /sys/kernel/debug/tracing/set_graph_function

# 开启跟。echo 1 > /sys/kernel/debug/tracing/tracing_on

# 触发操作后查看cat /sys/kernel/debug/tracing/trace

输出示例。

0)              |  my_probe() {
0)              |    my_init_hw() {
0)   0.500 us   |      my_configure();
0)   1.200 us   |    }
0)   2.100 us   |  }

大括号表示调用层级,时间表示函数执行耗时。一眼就能看出哪个函数慢。

tracepoint:静态跟踪点

tracepoint是内核预定义的跟踪点,比function tracer开销更低,语义更明确。

# 查看可用的tracepoint
ls /sys/kernel/debug/tracing/events/

# 常用目录。#   irq/        - 中断事件
#   sched/      - 调度事件
#   net/        - 网络事件
#   block/      - 块设备事。#   syscalls/   - 系统调用

# 启用某个tracepoint
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable

# 启用整个子系。echo 1 > /sys/kernel/debug/tracing/events/sched/enable

# 查看结果
cat /sys/kernel/debug/tracing/trace

# 关闭
echo 0 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable

trace-cmd工具

直接操作ftrace的sysfs接口比较繁琐,trace-cmd封装了常用操作:

# 安装
sudo apt install trace-cmd

# 跟踪函数调用(等价于function tracer。sudo trace-cmd record -p function -l my_probe

# 跟踪调用。sudo trace-cmd record -p function_graph -l my_probe

# 跟踪tracepoint
sudo trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit

# 查看结果
sudo trace-cmd report

# 录制5。sudo trace-cmd record -p function -l my_probe sleep 5

ftrace操作流程

flowchart TB A(["选择tracer"]) --> B{"tracer类型?"} B -->|"函数调用"| C["echo function<br/>> current_tracer"] B -->|"调用" | D["echo function_graph<br/>> current_tracer"] B -->|"静态跟踪点"| E["echo 1<br/>> events/xxx/enable"] C --> F["设置过滤<br/>set_ftrace_filter"] D --> G["设置过滤<br/>set_graph_function"] E --> H["选择事件<br/>events/子目"] F --> I["echo 1 > tracing_on"] G --> I H --> I I --> J["触发操作"] J --> K["cat trace"] K --> L["echo 0 > tracing_on"] style A fill:#E3F2FD style B fill:#FFF8E1 style L fill:#E8F5E9 style I fill:#F3E5F5

四、kprobes探针

ftrace只能跟踪函数入口和出口,kprobes能在函数的任意位置插入断点,还能读取和修改寄存器、局部变量。kprobes分两种:kprobe(函数入口,任意指令处触发)和kretprobe(函数返回时触发)。

基本原理

kprobes把目标指令替换成断点指令(x86上是int3),CPU执行到这里触发异常,kprobes的handler被调用。handler执行完后恢复原始指令继续执行。

register_kprobe/unregister_kprobe

/* kprobe示例:在do_sys_open入口打印信息(Linux 6.8*/
#include <linux/module.h>
#include <linux/kprobes.h>

static int kp_handler(struct kprobe *p, struct pt_regs *regs)
{
    pr_info("do_sys_open hit: filename_ptr=0x%lx\n",
            regs_get_kernel_argument(regs, 0));
    return 0;
}

static struct kprobe kp = {
    .symbol_name = "do_sys_open",
    .pre_handler = kp_handler,
};

static int __init kp_init(void)
{
    int ret;

    ret = register_kprobe(&kp);
    if (ret < 0) {
        pr_err("register_kprobe failed: %d\n", ret);
        return ret;
    }
    pr_info("kprobe registered at %pS\n", kp.addr);
    return 0;
}

static void __exit kp_exit(void)
{
    unregister_kprobe(&kp);
    pr_info("kprobe unregistered\n");
}

module_init(kp_init);
module_exit(kp_exit);
MODULE_LICENSE("GPL");

regs_get_kernel_argument(regs, n)获取第n个参数。x86_64调用约定:rdi、rsi、rdx、rcx、r8、r9依次是第1~6个参数。

kretprobe:函数返回时触发

/* kretprobe示例:监控kmalloc返回值(Linux 6.8*/
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    /* 返回值在x86_64的rax寄存器中 */
    unsigned long retval = regs_return_value(regs);

    pr_info("kmalloc returned: %pS (size info in entry handler)\n",
            (void *)retval);
    return 0;
}

static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    /* 获取kmalloc的第一个参数:size */
    unsigned long size = regs_get_kernel_argument(regs, 0);

    pr_info("kmalloc called: size=%lu\n", size);
    return 0;
}

static struct kretprobe krp = {
    .handler        = ret_handler,
    .entry_handler  = entry_handler,
    .maxactive      = 20,   /* 最大并发实例数 */
};

static int __init krp_init(void)
{
    int ret;

    krp.kp.symbol_name = "kmalloc";
    ret = register_kretprobe(&krp);
    if (ret < 0) {
        pr_err("register_kretprobe failed: %d\n", ret);
        return ret;
    }
    pr_info("kretprobe registered for kmalloc\n");
    return 0;
}

static void __exit krp_exit(void)
{
    unregister_kretprobe(&krp);
    pr_info("kretprobe unregistered, missed: %lu\n", krp.nmissed);
}

module_init(krp_init);
module_exit(krp_exit);
MODULE_LICENSE("GPL");

maxactive是关键参数——kmalloc可能被多个CPU并发调用,maxactive决定同时能跟踪多少个并发实例。设小了会miss,nmissed记录了遗漏次数。

完整示例:监控kmalloc调用

把kprobe和kretprobe组合起来,统计kmalloc的调用次数和分配大小。

/* kmalloc监控模块(Linux 6.8验证:*/
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/spinlock.h>

static atomic_t alloc_count = ATOMIC_INIT(0);
static atomic_t total_size  = ATOMIC_INIT(0);

static int kmalloc_entry(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    unsigned long size = regs_get_kernel_argument(regs, 0);
    atomic_inc(&alloc_count);
    atomic_add(size, &total_size);
    return 0;
}

static int kmalloc_return(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    unsigned long ptr = regs_return_value(regs);
    if (!ptr)
        pr_warn("kmalloc returned NULL!\n");
    return 0;
}

static struct kretprobe kmalloc_krp = {
    .entry_handler  = kmalloc_entry,
    .handler        = kmalloc_return,
    .maxactive      = 64,
    .kp.symbol_name = "kmalloc",
};

static int __init monitor_init(void)
{
    int ret = register_kretprobe(&kmalloc_krp);
    if (ret) {
        pr_err("register_kretprobe failed: %d\n", ret);
        return ret;
    }
    pr_info("kmalloc monitor started\n");
    return 0;
}

static void __exit monitor_exit(void)
{
    unregister_kretprobe(&kmalloc_krp);
    pr_info("kmalloc stats: calls=%d total_size=%d missed=%lu\n",
            atomic_read(&alloc_count),
            atomic_read(&total_size),
            kmalloc_krp.nmissed);
}

module_init(monitor_init);
module_exit(monitor_exit);
MODULE_LICENSE("GPL");

注意:生产环境别对kmalloc这种高频函数插kprobe,开销很大。调试时用用就行。

五、KGDB远程调试

printk和ftrace。看日。的思路,KGDB是真正的源码级调试——断点、单步、查看变量,和用户态gdb一样。

来自 linuxros.cn · linuxROS

内核配置

# 必须开启的配置。CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y    # 串口调试
# 可。CONFIG_KGDB_KDB=y               # KDB文本模式调试。CONFIG_DEBUG_INFO=y             # 调试符号。g。CONFIG_FRAME_POINTER=y          # 帧指针,保证backtrace准确

编译内核时确保CONFIG_DEBUG_INFO=y,否则gdb看不到源码和变量。

启动参数

# 在GRUB的linux行添加:
kgdboc=ttyS0,115200    # 指定串口和波特率
# 或者用ttyUSB(USB转串口)
kgdboc=ttyUSB0,115200

# 如果要KDB模式(不需要另一台机器)
kgdboc=ttyS0,115200 kgdbwait
# kgdbwait让内核启动时等待gdb连接

gdb连接

目标机上运行内核,开发机上用gdb连接vmlinux。

# 开发机。gdb vmlinux

# 连接目标机(通过串口线)
(gdb) target remote /dev/ttyS0

# 或者通过网络(需要kgdboe配置。(gdb) target remote 192.168.1.100:5555

连接成功后目标机内核被冻住,开发机的gdb获得完全控制权限

常用gdb命令

# 断点
(gdb) break my_probe              # 函数断点
(gdb) break my_drv.c:42           # 行号断点
(gdb) break *0xffffffffc0123456   # 地址断点
(gdb) info breakpoints            # 查看断点
(gdb) delete 1                    # 删除断点1

# 执行控制
(gdb) continue                    # 继续运行
(gdb) step                        # 单步进入函数
(gdb) next                        # 单步跳过函数
(gdb) finish                      # 运行到当前函数返。
# 查看数据
(gdb) print my_struct->field      # 打印结构体字。(gdb) print *ptr@10               # 打印数组。0个元。(gdb) print /x regs->rax          # 十六进制打印
(gdb) x/10xw 0xffff888000123400  # 查看内存

# 调用。(gdb) backtrace                   # 完整调用。(gdb) backtrace 5                 # 只看。。(gdb) frame 3                     # 切到。。(gdb) info locals                 # 当前帧的局部变更
# 硬件断点(用于调试MMIO区域。(gdb) hbreak *0xffff888000123400  # 硬件断点

KGDB调试流程

flowchart TB A(["编译内核<br/>DEBUG_INFO=y"]) --> B["启动参数<br/>kgdboc=ttyS0,115200"] B --> C["目标机运行内存"] C --> D["触发断点<br/>echo g > /proc/sysrq-trigger"] D --> E["开发机gdb连接<br/>target remote /dev/ttyS0"] E --> F["设断。单步/查看变量"] F --> G{"调试完成?"} G -->|"| F G -->|"| H["continue继续运行"] H --> I(["断开连接"]) style A fill:#E3F2FD style D fill:#FFF8E1 style I fill:#E8F5E9 style G fill:#FFF8E1

触发KGDB的两种方式:
1. 代码中主动触*:kgdb_breakpoint(),放在驱动的probe或ioctl。2. 运行时手动触*:echo g > /proc/sysrq-trigger,内核立即冻住等gdb连接

六、内核oops分析

oops是内核遇到非法操作(空指针、非法地址访问等)时打印的错误报告。学会读oops,定位bug的效率翻倍。

oops信息解读

一个典型的空指针解引用oops。

BUG: unable to handle page fault for address: 0000000000000018
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 0 P4D 0
Oops: 0000 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 1234 Comm: my_app Tainted: G           O
RIP: 0010:my_process_data+0x28/0x60 [my_driver]
Code: 48 89 c7 48 8b 40 18 83 f8 01 74 0a 48 8b 47 08 5d c3 <48> 8b 47 18 5d c3
RSP: 0018:ffffc90000123bf0 EFLAGS: 00010246
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000000 RSI: ffff888012345678 RDI: 0000000000000000
Call Trace:
 <TASK>
 ? show_regs+0x6d/0x80
 ? my_ioctl+0x45/0x80 [my_driver]
 ? do_vfs_ioctl+0xa5/0x680
 ? __x64_sys_ioctl+0x6e/0xb0
 ? do_syscall_64+0x3d/0x90
 ? entry_SYSCALL_64_after_hwframe+0x6e/0x76
 </TASK>

关键信息逐行解读。
| 字段 | | 含义 |
|:-----|:---|:-----|
| unable to handle page fault for address | 0x0000000000000018 | 访问的非法地址,偏。x18说明是结构体指针为NULL后访问了1个字 |
| RIP | my_process_data+0x28/0x60 | 出错函数和偏移,函数大小0x60,出错在偏移0x28|
| RDI | 0x0000000000000000 | x86_64第一个参数,是NULL |
| RAX | 0x0000000000000000 | 返回值或中间结果,也是NULL |
| Call Trace | my_ioctl→do_vfs_ioctl。.. | 调用链,从下往上读 |

0x18偏移 + RDI为NULL:大概率是ptr->field,ptr是NULL,field在结构体偏移0x18处。

addr2line:地址转源码行。

# 用oops中的RIP地址定位源码
# 注意:需要用模块。ko文件(或vmlinux对于内建代码。addr2line -e my_driver.ko 0x28

# 输出示例。# /home/user/my_driver.c:42

# 如果地址是内核函数的
addr2line -e vmlinux ffffffff81234567

# 显示函数。addr2line -e my_driver.ko -f 0x28
# 输出。# my_process_data
# /home/user/my_driver.c:42

objdump反汇。

# 反汇编出错的函数
objdump -d -S my_driver.ko | grep -A 20 "my_process_data>"

# -S选项混合源码和汇编(需要编译时。g。# 输出示例。# 0000000000000000 <my_process_data>:
#    0:   55                      push   %rbp
#    ...
#   28:   48 8b 47 18             mov    0x18(%rdi),%rax   <-- 这里崩溃
#   ...

0x18(%rdi)就是ptr->field,rdi是NULL。x18偏移正好和oops中的地址吻合。

decodecode脚本

内核源码自带scripts/decodecode,自动把oops中的Code行反汇编。

# 把oops中的Code行保存到文件
echo "Code: 48 89 c7 48 8b 40 18 83 f8 01 74 0a 48 8b 47 08 5d c3 <48> 8b 47 18 5d c3" > oops_code.txt

# 用decodecode反汇。scripts/decodecode < oops_code.txt

# 输出会标注出错的指令。>包裹的那条)
# All code:
#   0:   48 89 c7                mov    %rax,%rdi
#   3:   48 8b 40 18             mov    0x18(%rax),%rax
#   ...
#   1c:   48 8b 47 08            mov    0x8(%rdi),%rax
#   20:   5d                      pop    %rbp
#   21:   c3                      ret
# Code starting with the faulting instruction:
#   0:   48 8b 47 18             mov    0x18(%rdi),%rax
#   4:   5d                      pop    %rbp
#   5:   c3                      ret

oops分析流程

flowchart TB A(["收到oops"]) --> B["读RIP<br/>定位出错函数"] B --> C["读非法地址<br/>判断空指。越界"] C --> D["读寄存器<br/>找参数"] D --> E["读Call Trace<br/>理清调用"] E --> F["addr2line<br/>转源码行"] F --> G["objdump -d<br/>反汇编确"] G --> H["定位根因<br/>修复代码"] style A fill:#FFEBEE style H fill:#E8F5E9 style C fill:#FFF8E1 style F fill:#F3E5F5

七、内存调试

内存问题是最难调的bug类型之一——泄漏、越界、use-after-free,症状和原因往往隔了十万八千里。Linux内核提供了三个层次的内存调试工具。

kmemleak:内核内存泄漏检。

kmemleak的工作原理类似垃圾回收器——定期扫描内核内存,找没有被任何指针引用的已分配块,这些就是疑似泄漏。

# 内核配置
# CONFIG_DEBUG_KMEMLEAK=y

# 手动触发扫描
echo scan > /sys/kernel/debug/kmemleak

# 查看泄漏报告
cat /sys/kernel/debug/kmemleak

# 输出示例。# unreferenced object 0xffff888012345600 (size 64):
#   comm "my_app", pid 1234, jiffies 4294912345
#   hex dump (first 32 bytes):
#     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
#     01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
#   backtrace:
#     [<ffffffff81234567>] kmalloc_trace+0x27/0x90
#     [<ffffffffc0123456>] my_probe+0x45/0x80
#     [<ffffffff81234568>] pci_device_probe+0x78/0x120

# 清空当前报告(确认不是误报后。echo clear > /sys/kernel/debug/kmemleak

# 只看某个地址的详。echo 0xffff888012345600 > /sys/kernel/debug/kmemleak

backtrace直接告诉你哪行代码分配的内存没释放。my_probe+0x45就是泄漏点。

slub_debug:slab分配器调试

slub_debug在分配的slab对象周围加红区(redzone),检测越界读写;加毒值(poison),检测use-after-free。

# 内核启动参数,全局开销slub_debug=FZP

# 参数含义。# F - 对已释放对象填充毒值(检测use-after-free。# Z - 红区检查(检测越界写。# P - 慢速路径(更严格的检查)
# U - 用户追踪(记录分支释放调用栈)
# T - 追踪(记录所有分支释放。
# 只对特定slab开销slub_debug=FZP,kmalloc-64

# 运行时查看slab信息
cat /sys/kernel/slab/kmalloc-64/trace
cat /sys/kernel/slab/kmalloc-64/alloc_calls
cat /sys/kernel/slab/kmalloc-64/free_calls

# 手动触发检。echo 1 > /sys/kernel/slab/kmalloc-64/validate

slub_debug检测到问题会直接打oops,信息很明确。

BUG kmalloc-64 (Tainted: G    B            ): Redzone overwritten

KASAN:内核地址消毒。

KASAN(Kernel Address SANitizer)是最强的内存错误检测工具,能检测越界读写、use-after-free、栈溢出,开销比slub_debug大但检测能力也强得多。

# 内核配置
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y    # 通用模式,兼容性好
# 或。CONFIG_KASAN_SW_TAGS=y    # 基于软件标签(arm64专用,开销更小。
# 编译安装新内核后启动
# KASAN会自动在每次内存访问时检```

KASAN检测到错误会打印详细报告:

BUG: KASAN: slab-use-after-free in my_process_data+0x35/0x60
Read of size 4 at addr ffff888012345680 by task my_app/1234

CPU: 0 PID: 1234 Comm: my_app Tainted: G B
Call trace:
dump_stack+0x7c/0xa0
print_report+0x178/0x490
kasan_report+0xb0/0x110
my_process_data+0x35/0x60

Allocated by task 1234:
kasan_save_stack+0x24/0x50
kmem_cache_alloc+0x158/0x340
my_probe+0x45/0x80

Freed by task 1234:
kasan_save_stack+0x24/0x50
kasan_save_free_info+0x28/0x50
kmem_cache_free+0x124/0x2a0
my_remove+0x30/0x60

报告直接告诉你:谁分配的、谁释放的、谁在释放后又访问了。三条调用栈一出,bug基本锁死。
### 内存调试工具对比

| 工具 | 检测能力 | 开销 | 适用阶段 |
|:-----|:---------|:-----|:---------|
| kmemleak | 内存泄漏 | 低(扫描时短暂停顿) | 长时间运行后检测 |
| slub_debug | 越界、use-after-free | 中(5%~20%) | 开发调试 |
| KASAN | 越界、use-after-free、栈溢出 | 高(2x内存+20%速度) | 开发调试、CI |

## 八、调试工具对。
| 工具 | 适用场景 | 运行时开销 | 侵入性 | 需要重编译 | 学习成本 |
|:-----|:---------|:----------|:------|:----------|:---------|
| printk | 快速定位、简单跟踪 | 极低 | 低(加打印语句) | | |
| 动态调试 | pr_debug运行时开销 | 极低(关闭时) | | 否(需CONFIG_DYNAMIC_DEBUG| |
| ftrace | 函数调用链、性能分析 | 低~| | | |
| kprobes | 任意函数探针、变量读 | 中~| | | |
| KGDB | 源码级调试、断点单 | 无(断住时) | | 是(需CONFIG_KGDB| |
| oops分析 | 崩溃定位 | | | | |
| kmemleak | 内存泄漏检 | | | 是(需CONFIG_DEBUG_KMEMLEAK| |
| slub_debug | 越界/use-after-free | | | 是(启动参数 | |
| KASAN | 全面内存错误检 | | | 是(需CONFIG_KASAN| |

选工具的原则*开销从小到大试,侵入性从低到高用**。先printk,不行上ftrace,还不行再kprobes,最后才上KGDB。内存问题直接KASAN,别折腾。
## 九、常见问题
### Q1:ftrace无输出?

最常见的原因是`tracing_on`没开销
```bash
# 检查tracing_on状态cat /sys/kernel/debug/tracing/tracing_on
# 输出0说明没开

# 开销echo 1 > /sys/kernel/debug/tracing/tracing_on

# 其他可能原因。# 1. current_tracer设成了nop
cat /sys/kernel/debug/tracing/current_tracer
# 输出应该是function或function_graph,不是nop

# 2. 过滤器太严,没有匹配的函数cat /sys/kernel/debug/tracing/set_ftrace_filter
# 清空过滤器(跟踪所有函数)
echo > /sys/kernel/debug/tracing/set_ftrace_filter

# 3. 缓冲区满。cat /sys/kernel/debug/tracing/trace_pipe
# 用trace_pipe读取不会丢失数据

Q2:KGDB连接不上。

按这个清单排查:

# 1. 确认串口线接。# 开发机:ttyS0或ttyUSB0
# 目标机:kgdboc指定的串。
# 2. 确认波特率一。# kgdboc=ttyS0,115200 中的115200必须和gdb端一。stty -F /dev/ttyS0 115200

# 3. 确认内核配置
grep CONFIG_KGDB /boot/config-$(uname -r)
# CONFIG_KGDB=y
# CONFIG_KGDB_SERIAL_CONSOLE=y

# 4. 确认kgdboc参数生效
cat /proc/cmdline | grep kgdboc

# 5. 确认目标机已进入KGDB模式
# 目标机上执行。echo g > /proc/sysrq-trigger
# 或者代码中调用 kgdb_breakpoint()

# 6. 串口被其他程序占。fuser /dev/ttyS0
# 如果有输出,先kill掉占用的进程

Q3:kmemleak误报。

kmemleak基于指针扫描,有些合法的内存管理方式会被误判。

# 常见误报场景:# 1. 内存池:预分配一批对象存在链表里,kmemleak看不到链表外的引。# 2. 通过物理地址访问:ioremap映射的内存,kmemleak扫不。# 3. 指针被编码存储:比如低几位用作标志位,kmemleak认不。
# 处理方法。# 1. 确认是误报后,清空报。echo clear > /sys/kernel/debug/kmemleak

# 2. 对已知误报的地址,添加白名单
echo scan > /sys/kernel/debug/kmemleak    # 重新扫描
# 然后只关注新增的泄漏

# 3. 代码中主动告诉kmemleak某块内存不是泄漏
kmemleak_not_leak(ptr);
kmemleak_ignore(ptr);

十、总结

调试场景速查表

调试场景 推荐工具 操作速查
快速加打印看变更 printk/pr_err pr_info("val=%d\n", val)
运行时开关调试输 动态调试 echo 'file xxx.c +p' > dynamic_debug/control
梳理函数调用途 ftrace function echo function > current_tracer
分析函数执行耗时 ftrace function_graph echo function_graph > current_tracer
在任意函数插探针 kprobes register_kprobe(&kp)
源码级断点调试 KGDB target remote /dev/ttyS0
崩溃后定位代码行 oops + addr2line addr2line -e xxx.ko 0x28
内存泄漏 kmemleak echo scan > kmemleak
越界/use-after-free KASAN CONFIG_KASAN=y 编译内核

调试工具选择流程

flowchart TB A(["遇到bug"]) --> B{"什么类。"} B -->|"功能逻辑 | C["printk/动态调试<br/>加打印看流程"] B -->|"调用链不 | D["ftrace<br/>跟踪函数调用"] B -->|"需要读变量"| E["kprobes<br/>插探针读寄存"] B -->|"需要断点单 | F["KGDB<br/>源码级调度"] B -->|"内核崩溃"| G["oops分析<br/>addr2line定位"] B -->|"内存泄漏"| H["kmemleak<br/>扫描泄漏"] B -->|"越界/UAF"| I["KASAN<br/>全面检"] C --> J(["定位修复"]) D --> J E --> J F --> J G --> J H --> J I --> J style A fill:#E3F2FD style B fill:#FFF8E1 style J fill:#E8F5E9 style G fill:#FFEBEE style I fill:#FFEBEE

调试内核没有银弹,但工具链是完整的。printk解决80%的问题,ftrace和kprobes解决15%,剩。%的疑难杂症交给KGDB和KASAN。关键是根据问题类型选对工具,别拿printk去调内存泄漏,也别上KGDB去查一个printk就能看出来的逻辑错误。

本文首发于linuxros.cn,转载请注明出处。

版权声明

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