本文地址:https://www.ebpf.top/post/bpf_rawtracepoint

1. eBPF Trace 跟踪常见的 Hook 类型

通过eBPF可以对多种类型的事件进行跟踪,在 trace 领域分类如下:

  • 内核静态跟踪点 tracepoint/rawtracepoint/btf-tracepoint
    • 参见 /sys/kernel/tracing/available_events
  • 内核动态跟踪点 k[ret]probefentry/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)。rawtracepointtracepoint 相比直接暴露原始参数,一定程度上避免创建稳定跟踪点参数的带来的性能开销,但由于直接对用户暴露了原始参数,因此这是属于动态跟踪的模式,属于不稳定的跟踪模式。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 工具运行并绘制的(运行需要提前编译内核代码),纵坐标是每秒运行的指令数:

perf comparision of linux trace

性能压测运行方式如下:

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_entersys_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 跟踪点通过 tracepointrawtracepoint 跟踪参数对比为例,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);
}

参考