本文地址:https://www.ebpf.top/post/bpf_rawtracepoint
1. eBPF Trace 跟踪常见的 Hook 类型
通过eBPF
可以对多种类型的事件进行跟踪,在 trace 领域分类如下:
- 内核静态跟踪点
tracepoint
/rawtracepoint
/btf-tracepoint
- 参见
/sys/kernel/tracing/available_events
- 内核动态跟踪点
k[ret]probe
, fentry/fexit
(基于 BTF)
- Kprobe
/sys/kernel/tracing/available_filter_functions
- 用户空间静态跟踪点 USDT
- 查看方式
readelf -n
或 bpftrace 工具 bpftrace -l 'usdt:/home/dave/ebpf/linux-tracing/usdt/main:*'
- 用户空间动态跟踪:
u[ret]probe
,可通过 nm hello | grep main
查看
- 性能监控计数器 PMC
perf_event
本文我们重点讨论一下内核静态跟踪中的 rawtracepoint
,最后我们基于 libbpf 开发库和 bpftrace 给出实际代码样例。
2. BPF 原始跟踪点 rawtracepoint
eBPF 的作者 Alexei Starovoitov 在 Linux 内核 4.17 版本中添加了一个原始跟踪点(rawtracepoint)。rawtracepoint
与 tracepoint
相比直接暴露原始参数,一定程度上避免创建稳定跟踪点参数的带来的性能开销,但由于直接对用户暴露了原始参数,因此这是属于动态跟踪的模式,属于不稳定的跟踪模式。rawtracepoint
相比较 kprobe
来讲相对稳定,因为跟踪点无论是名字还是参数变化相对低频,而相对于 tracepoint
的跟踪方式可提供更优的性能。rawtracepoint
提交实现可参见:bpf: introduce BPF_RAW_TRACEPOINT。从作者提交的性能压测报告相比较 kprobe 和 tracepoint 跟踪都具有性能提升,比较适用于长期监控频繁次调用的函数,比如系统调用。Tracee 安全产品监控系统调用的实现就采用 rawtracepoint 的方式。
2.1 跟踪性能优化提升 20%
表格为作者提交时候的原始性能数据:
1
2
3
|
tracepoint base kprobe+bpf tracepoint+bpf raw_tracepoint+bpf
task_rename 1.1M 769K 947K 1.0M
urandom_read 789K 697K 750K 755K
|
下图的数据是我基于内核代码中官方提供的 bench 工具运行并绘制的(运行需要提前编译内核代码),纵坐标是每秒运行的指令数:
性能压测运行方式如下:
1
2
|
$ cd tools/testing/selftests/bpf
$ ./benchs/run_bench_trigger.sh
|
2.2 rawtracepoint 跟踪事件查看及数量统计
bpftrace 在 0.19 版本支持 rawtracepoint。 可以使用 bpftrace -l 查看,程序类型缩写为 rt,参数类型为 arg0, arg1..。我们可使用 bpftrace -l 查看到全部的列表:
1
|
$ sudo bpftrace -l "rawtracepoint:*"
|
在 Ubuntu 22.04 系统中 (内核版本 6.2) 系统中大概有 1480 多个:
1
2
3
4
5
|
$ sudo bpftrace -l "rawtracepoint:*"|wc -l
1480
$ sudo bpftrace -l "tracepoint:*"|wc -l
2124
|
细心的童靴,可能以已经注意到系统中的 tarcepoint 事件却有 2124 个,这是什么原因导致的呢?
在 bpftrace 中是如何获取到 rawtracepoint
呢? 通过分析源码,我们可以得知其实 bpftrace 中是读取了 /sys/kernel/debug/tracing/available_events
文件中的所有跟踪点,同时排除一部分以 syscalls:sys_enter_
或者 syscalls:sys_exit_
开头的跟踪事件。 之所以要排查是因为有两个特殊情况,是因为:
- 统一用
sys_enter
表示 syscalls
分类下的 sys_enter_xxx
事件: SEC("raw_tracepoint/sys_enter")
- 统一用
sys_exit
表示 syscalls
分类下的 sys_exit_xxx
事件: SEC("raw_tracepoint/sys_exit")
即可以用 sys_enter
和 sys_exit
事件来监控所有系统调用事件。
可以通过查看 /sys/kernel/debug/tracing/available_events
文件的内容找到 rawtracepoint
可监控的事件。 文件中每行内容的格式是:
1
2
|
# <category>:<name>
skb:kfree_skb
|
但是,在 rawtracepoint
用到的是 <name>
的值,而不是整个 <category>:<name>
:例如
1
2
|
$ bpftrace -e 'rawtracepoint:kfree_skb { printf("%s\n", comm)} '
Attaching 1 probe...
|
2.3 传递参数变化
从 BPF 程序角度来看,rawtracepoint
方式的参数定义和访问如下所示,后续我们将给出完整的使用样例程序。
1
2
3
4
5
6
7
8
9
|
struct bpf_raw_tracepoint_args {
__u64 args[0];
};
int bpf_prog(struct bpf_raw_tracepoint_args *ctx)
{
// program can read args[N] where N depends on tracepoint
// and statically verified at program load+attach time
}
|
所有的参数都会通过数组指针的方式传入。这里我们基于 __set_task_comm
函数中定义的 task_rename
跟踪点通过 tracepoint
和 rawtracepoint
跟踪参数对比为例,task_rename
跟踪点函数在内核中的函数声明如下:
1
2
|
void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec);
// rawtracepoint 方式下直接压入原始参数 tsk/buf/exec
|
如果系统没有 task_rename 事件,我们可以编译如下程序手工触发验证测试:
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
31
32
33
34
35
36
37
|
// gcc -o rename test_rename.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MAX_CNT 1
static void test_task_rename(int cpu)
{
char buf[] = "test\n";
int i, fd;
fd = open("/proc/self/comm", O_WRONLY|O_TRUNC);
if (fd < 0) {
printf("couldn't open /proc\n");
exit(1);
}
for (i = 0; i < MAX_CNT; i++) {
if (write(fd, buf, sizeof(buf)) < 0) {
printf("task rename failed: %s\n", strerror(errno));
close(fd);
return;
}
}
close(fd);
}
int main()
{
test_task_rename(0);
return 0;
}
|
3. BPF 程序中使用 rawtracepoint 样例
3.1 libbpf 库 (基于 CO-RE)
系统中对应的 task_rename
跟踪点为 tracepoint:task:task_rename
,跟踪点格式定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ cat /sys/kernel/debug/tracing/events/task/task_rename/format
name: task_rename
ID: 131
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:pid_t pid; offset:8; size:4; signed:1;
field:char oldcomm[16]; offset:12; size:16; signed:0;
field:char newcomm[16]; offset:28; size:16; signed:0;
field:short oom_score_adj; offset:44; size:2; signed:1;
print fmt: "pid=%d oldcomm=%s newcomm=%s oom_score_adj=%hd", REC->pid, REC->oldcomm, REC->newcomm, REC->oom_score_adj
|
我们可以通过 libbpf 库在程序中使用结构,编写的代码如下所示:
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
|
/* from: vmlinux.h
struct trace_entry {
short unsigned int type;
unsigned char flags;
unsigned char preempt_count;
int pid;
};
struct trace_event_raw_task_rename {
struct trace_entry ent;
pid_t pid;
char oldcomm[16];
char newcomm[16];
short int oom_score_adj;
char __data[0];
};
*/
SEC("tracepoint/task/task_rename")
int prog(struct trace_event_raw_task_rename *ctx)
{
bpf_printk("task_rename -> pid %d, oldcomm %s, newcomm %s, oom %d",
ctx->pid,
ctx->oldcomm,
ctx->newcomm,
ctx->oom_score_adj );
return 0;
}
|
如果使用 rawtracepoint
的方式,则是将 __set_task_comm(struct task_struct *tsk, const char *buf, bool exec)
的参数依次压入 bpf_raw_tracepoint_args
结构中,args[0] 为参数 struct task_struct *tsk
, args[1] 为 const char *buf
,这里代表重命名的 comm_name
,其他参数依次类推。
bpf_raw_tracepoint_args
参数结构如下:
1
2
3
|
struct bpf_raw_tracepoint_args {
__u64 args[0];
};
|
在使用 raw_tracepoint 方式跟踪的代码编写如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
SEC("raw_tracepoint/task_rename")
int rt_prog(struct bpf_raw_tracepoint_args *ctx)
{
// void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec);
struct task_struct *tsk = (struct task_struct *) ctx->args[0];
u32 pid;
u16 oom_score_adj;
char old_name[TASK_COMM_LEN] = {};
char new_name[TASK_COMM_LEN] = {};
pid = BPF_CORE_READ(tsk, pid);
// BPF_CORE_READ_INTO(&old_name, tsk, comm);
bpf_core_read(&old_name, sizeof(old_name), &tsk->comm);
bpf_core_read(&new_name, sizeof(new_name), (void *)ctx->args[1]);
oom_score_adj = BPF_CORE_READ(tsk, signal, oom_score_adj);
bpf_printk("task_rename:rt -> pid %d, oldcomm %s, newcomm %s, oom %d",
pid,
old_name,
new_name,
oom_score_adj);
return 0;
}
|
3.2 bpftrace 样例代码
bpftrace 在 0.19 版本开始支持 rawtracepoint。 可以使用 bpftrace -l 查看,程序类型缩写为 rt,参数类型为 arg0, arg1..
1
2
|
$ bpftrace --version
bpftrace v0.20.0
|
bpftrace 通过 tracepoint:task:task_rename
方式跟踪:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# bpftrace -e 'tracepoint:task:task_rename { printf("enter t:task:task_rename %s, pid %d, oldcommn %s, newcomm %s, oom 0x%x\n", comm, args->pid, args->oldcomm, args->newcomm, args->oom_score_adj); }'
$ sudo bpftrace -e 'tracepoint:task:task_rename
{
printf("enter t:task:task_rename %s,
pid %d, oldcomm %s, newcomm %s, oom 0x%x\n",
comm,
args->pid,
args->oldcomm,
args->newcomm
args->oom_score_adj); }'
Attaching 1 probe..
enter t:task:task_rename x11vnc, pid 3774, oldcommn x11vnc, newcomm x11vnc, oom 0x0
|
bpftrace 通过 rawtracepoint:task_rename
方式跟踪:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# rename.bt
rawtracepoint:task_rename
{
$task = (struct task_struct *)arg0;
$pid = $task->pid;
$oom_score_adj = $task->signal->oom_score_adj;
printf("enter rt:task:task_rename %s, pid %d, oldcommn %s, newcomm %s, oom 0x%x\n",
comm,
$pid,
$task->comm,
str(arg1),
$oom_score_adj);
}
|
参考