AkiraZheng's Time.

linux内核驱动debug方法

Word count: 1.6kReading time: 7 min
2025/12/24

当我们需要查看内核中函数是否调用、入参值是否符合预期、函数返回值、函数调用栈等问题时,我们可以在启动机器后,通过trace方法来动态获取这些信息。

ftrace是最常用的工具,其中kprobetracepoint是其中的两个子工具。

kprobe

内核中几乎所有函数都可以被kprobe拦截,包括内核初始化时的函数。kprobe可以用来跟踪函数的调用、入参值和返回值,且不需要手动写trace代码

cd /sys/kernel/debug/tracing

查看

查看所有kprobe可用的函数(几乎所有内核函数都在available_filter_functions中):

1
cat available_filter_functions | grep __setup_irq

当前已经添加跟踪的事件:

1
2
cat kprobe_events

禁用所有已存在的 kprobe 事件

1
2
3
# 禁用所有已存在的 kprobe 事件(关键步骤)
# 这会关闭所有 events/kprobes/ 下的 enable 开关
echo 0 > events/kprobes/enable 2>/dev/null

添加 kprobe 事件到 kprobe_events

1
2
3
echo 'p:kprobes/__setup_irq __setup_irq irq=%x0 desc=%x1 new=%x2' > kprobe_events
cat kprobe_events # 此时就能看到刚刚添加的 kprobe 事件
ls events/kprobes/ | grep __setup_irq # 此时能看到 __setup_irq 事件了

过滤、禁止输出调用栈

1
2
3
echo 'name=="eth2"' > events/kprobes/filter  # 过滤入参 name==eth2 的事件
echo 'pid!=1234' > events/kprobes/filter # 过滤特定 pid 的事件
echo nostacktrace > /sys/kernel/debug/tracing/trace_options # 关闭调用栈的打印,只看 __setup_irq 的入参显示

启动追踪

1
2
echo 1 > events/kprobes/enable
echo 1 > tracing_on

查看输出

1
2
3
cat trace # 可以看到所有的 trace,包括耗时
cat trace_pipe # 实时采集管道,退出后就看不到了
cat trace_pipe | tee /tmp/trace.log # 使用tee同时输出到屏幕和文件

停止追踪

1
echo 0 > tracing_on

清空缓存

1
echo > trace

示例:使用 kprobe 跟踪__setup_irq

用 kprobe 查看__setup_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cd /sys/kernel/debug/tracing

# 禁用所有已存在的 kprobe 事件(关键步骤)
# 这会关闭所有 events/kprobes/ 下的 enable 开关
echo 0 > events/kprobes/enable 2>/dev/null

# 1. 查看当前已经添加跟踪的事件
cat kprobe_events

# 2. 使用简单ftrace查看
echo 'p:kprobes/__setup_irq __setup_irq irq=%x0 desc=%x1 new=%x2' > kprobe_events
# 刚开始 ./events/kprobes/路径 查不到有 __setup_irq
# 事件 echo 进 kprobe_events 后,在./events/kprobes/路径下就有这个 __setup_irq
echo nostacktrace > /sys/kernel/debug/tracing/trace_options # 关闭调用栈的打印,只看 __setup_irq 的入参显示
echo 1 > events/kprobes/enable
echo 1 > tracing_on

# 3. 查看输出
# cat trace # 可以看到所有的 trace,包括耗时
cat trace_pipe # 实时采集管道,退出后就看不到了
cat trace_pipe | tee /tmp/trace.log # 使用tee同时输出到屏幕和文件

# 4. 停止追踪
echo 0 > tracing_on

# 5. 清空缓存
echo > trace

tracepoint 内核插桩

其优点是可以按照我们预期地输出信息,且可以替代printk。缺点是需要内核源码中明确定义 tracepoint 事件,且无法像 kprobe 通过echo 'name=="eth2"' > events/kprobes/filter来过滤特定 pid 的 tracepoint 事件,需要在桩函数代码中自己实现过滤。

trace point 是内核中预定义的跟踪点,通常用于跟踪内核事件。与 kprobe 不同,tracepoint 需要内核源码中明确定义。并显式调用 tracepoint 事件。

有时我们需要printk来输出信息,但printk的输出量很大,且无法控制输出格式。而 tracepoint 可以通过 ftrace 来控制输出,且输出格式可以自定义。

tracepoint都有一个 name、一个 enable 开关和一系列桩函数。

tracepoint 结构体

./include/linux/tracepoint-defs.h中提供了tracepoint struct结构体来定义 tracepoint 的信息。

1
2
3
4
5
6
7
8
9
10
struct tracepoint {
const char *name; /* Tracepoint name */
struct static_key_false key;
struct static_call_key *static_call_key;
void *static_call_tramp;
void *iterator;
void *probestub;
struct tracepoint_func __rcu *funcs;
struct tracepoint_ext *ext;
};

创建 tracepoint

内核里已经自带实现了许多 tracepoint 桩函数,可以在/sys/kernel/debug/tracing/events/目录下看到。

1
2
root@akira:/sys/kernel/tracing# ls ./events/irq
enable filter irq_handler_entry irq_handler_exit softirq_entry softirq_exit softirq_raise

如果想实现添加自己的 tracepoint,可以参考下面的步骤:

首先要在内核中插件入 tracepoint,需要在内核源码中添加 tracepoint 的定义和实现。一般基于模块粒度创建一个trace头文件,本例中创建./include/trace/events/irq.

比如对于irq,可以在./include/trace/events/irq.h中添加 tracepoint 定义:

1
2
3
4
5
6
7
8
/* SPDX-License-Identifier: GPL-2.0 */
#undef TRACE_SYSTEM
#define TRACE_SYSTEM irq

#if !defined(_TRACE_IRQ_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_IRQ_H

#include <linux/tracepoint.h>

然后可以在./include/trace/events/irq.h中添加 tracepoint 的定义和实现,一个 tracepoint 文件可以包含多个事件,这里以irq_handler_exit为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 /**
* irq_handler_exit - called immediately after the irq action handler returns
* @irq: irq number
* @action: pointer to struct irqaction
* @ret: return value
*
* If the @ret value is set to IRQ_HANDLED, then we know that the corresponding
* @action->handler successfully handled this irq. Otherwise, the irq might be
* a shared irq line, or the irq was not handled successfully. Can be used in
* conjunction with the irq_handler_entry to understand irq handler latencies.
*/
TRACE_EVENT(irq_handler_exit, // tracepoint name 可自定义,后面使用时用 trace_irq_handler_exit 来调用

TP_PROTO(int irq, struct irqaction *action, int ret),

TP_ARGS(irq, action, ret), // 函数参数

TP_STRUCT__entry( // 表示当前 trace 函数定义了这些变量
__field( int, irq )
__field( int, ret )
),

TP_fast_assign( // 对前面定义的 trace 函数变量进行赋值
__entry->irq = irq;
__entry->ret = ret;
),

TP_printk("irq=%d ret=%s",
__entry->irq, __entry->ret ? "handled" : "unhandled") // tracepoint 的输出格式,也就是前面说的可以替代printk的地方
);

代码中调用 tracepoint

在内核代码中,可以通过trace_irq_handler_exit来调用 tracepoint。比如在./kernel/irq/handle.c中添加:

1
2
#include <trace/events/irq.h>
trace_irq_handler_exit(irq, action, res);

内核中动态查看相关桩函数的打印

查看可用的 tracepoint 以及它们的输出格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看所有 irq 相关的 tracepoints
ls /sys/kernel/tracing/events/irq/

# 查看特定 tracepoint 的格式
cat /sys/kernel/tracing/events/irq/irq_handler_exit/format

# 输出示例:
name: irq_handler_exit
ID: 1234
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int irq; offset:8; size:4; signed:1;
field:int ret; offset:12; size:4; signed:1;

启用和查看 tracepoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方法1:单个启用
echo 1 > /sys/kernel/tracing/events/irq/irq_handler_exit/enable

# 方法2:启用所有 irq 相关的 tracepoints
echo 1 > /sys/kernel/tracing/events/irq/enable

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

# 查看输出
cat /sys/kernel/tracing/trace
# 或实时查看
cat /sys/kernel/tracing/trace_pipe

# 停止跟踪
echo 0 > /sys/kernel/tracing/tracing_on
echo 0 > /sys/kernel/tracing/events/irq/irq_handler_exit/enable

也可以使用 perf 工具来查看 tracepoint 的输出:

1
2
3
4
5
6
7
8
9
10
11
# 查看所有可用的 tracepoints
sudo perf list | grep irq:

# 记录 tracepoint 事件
sudo perf record -e irq:irq_handler_exit -a sleep 10

# 实时查看
sudo perf trace -e irq:irq_handler_exit

# 统计事件计数
sudo perf stat -e irq:irq_handler_exit -a sleep 10
CATALOG
  1. kprobe
    1. 查看
    2. 禁用所有已存在的 kprobe 事件
    3. 添加 kprobe 事件到 kprobe_events
    4. 过滤、禁止输出调用栈
    5. 启动追踪
    6. 查看输出
    7. 停止追踪
    8. 清空缓存
    9. 示例:使用 kprobe 跟踪__setup_irq
  2. tracepoint 内核插桩
    1. tracepoint 结构体
    2. 创建 tracepoint
    3. 代码中调用 tracepoint
    4. 内核中动态查看相关桩函数的打印