eBPF学习记录(三)使用BCC开发eBPF程序

上一节,我们使用了bpftrace 开发eBPF程序跟踪内核和用户态的程序,bpftrace 简单易用,非常适合入门,可以带初学者轻松体验 eBPF 的各种跟踪特性。但是,bpftrace 并不适用于所有的 eBPF 应用,它本身的限制导致我们无法在需要复杂 eBPF 程序的场景中使用它。在复杂的应用中,还是推荐使用 BCC 或者 libbpf 进行开发。现在讲一下BCC 的开发,有问题可以看官方文档。

现在我们试试使用BCC开发一个eBPF程序,分以下3个步骤,

  1. 使用 C 开发一个 eBPF 程序
  2. 使用 Python 和 BCC 库开发一个用户态程序
  3. 执行 eBPF 程序

现在先来一个最简单的程序玩玩吧!

一、开发一个最简单的eBPF程序

  1. 使用 C 开发一个 eBPF 程序
    新建一个 hello.c 文件,并输入下面的内容:
int hello_world(void *ctx)
{
    bpf_trace_printk("Hello, World!");
    return 0;
}

就像所有编程语言的“ Hello World ”示例一样,这段代码的含义就是打印一句 “Hello, World!” 字符串。其中, bpf_trace_printk() 是一个最常用的 BPF 辅助函数,它的作用是输出一段字符串。不过,由于 eBPF 运行在内核中,它的输出并不是通常的标准输出(stdout),而是内核调试文件 /sys/kernel/debug/tracing/trace_pipe ,你可以直接使用 cat 命令来查看这个文件的内容。

  1. 使用 Python 和 BCC 库开发一个用户态程序

新建一个 hello.py 文件,并输入下面的内容:

#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF

# 2) load BPF program
b = BPF(src_file="hello.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) read and print /sys/kernel/debug/tracing/trace_pipe
b.trace_print()

让我们来看看每一处的具体含义:

  • 第 1) 处导入了 BCC 库的 BPF 模块,以便接下来调用;

  • 第 2) 处调用 BPF() 加载第一步开发的 BPF 源代码;

  • 第 3) 处将 BPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现;

  • 第 4) 处则是读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe的内容,并打印到标准输出中。

  • 执行 eBPF 程序
    用户态程序开发完成之后,最后一步就是执行它了。需要注意的是, eBPF 程序需要以 root 用户来运行,非 root 用户需要加上 sudo 来执行:

sudo python3 hello.py

稍等一会,你就可以看到如下的输出:

b'         python3-9659    [000] d... 67871.172937: bpf_trace_printk: Hello, World!'
b'         python3-9659    [000] d... 67871.173107: bpf_trace_printk: Hello, World!'
b'         python3-9659    [000] d... 67871.173302: bpf_trace_printk: Hello, World!'
b'           <...>-9661    [000] d... 67911.701439: bpf_trace_printk: Hello, World!'
b'     gnome-shell-1758    [001] d... 67912.214565: bpf_trace_printk: Hello, World!'
b'     gnome-shell-1758    [001] d... 67914.622574: bpf_trace_printk: Hello, World!'
b'     gnome-shell-1758    [001] d... 67914.622619: bpf_trace_printk: Hello, World!'

输出字符串含义解析:

  • python3-9659 表示进程的名字和 PID;
  • [000] 表示 CPU 编号;
  • d… 表示一系列的选项;
  • 67871.172937表示时间戳;
  • bpf_trace_printk 表示函数名;
  • 最后的 “Hello, World!” 就是调用bpf_trace_printk() 传入的字符串。

到了这里,我们已经成功开发并运行了第一个 eBPF 程序。

二、开发 BPF 事件映射程序
刚刚开发的第一个程序是使用bpf_trace_printk()输出,他输出格式不够灵活,输出的内容不太符合我们平常的需要。现在我们尝试一下使用时间映射把我们需要的数据映射到用户态,然后在python中选择我们需要的信息打印到控制台中,这里以追踪我们打开文件为例子尝试一下吧。
先看 eBPF 程序open.c的代码:

// 包含头文件
#include 
#include 

// 定义数据结构
struct data_t {
  u32 pid;
  u64 ts;
  char comm[TASK_COMM_LEN];
  char fname[NAME_MAX];
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);

// 定义kprobe处理函数
int bcc_do_sys_openat2(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
  struct data_t data = { };

  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();

  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
  {
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  }

  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

我们使用了BCC 定义的一系列的库函数和辅助宏定义,下面讲解一下:

  • BPF_PERF_OUTPUT:定义一个 Perf 事件类型的 BPF 映射,需要调用 perf_submit() 把数据提交到 BPF 映射中;
  • bpf_get_current_pid_tgid 用于获取进程的 TGID 和 PID;
  • bpf_ktime_get_ns 用于获取系统自启动以来的时间,单位是纳秒;
  • bpf_get_current_comm 用于获取进程名,并把进程名复制到预定义的缓冲区中;
  • bpf_probe_read 用于从指定指针处读取固定大小的数据,这里则用于读取进程打开的文件名。

现在写一个用户态程序open.py,用来读取 BPF 映射内容并输出到标准输出:

from bcc import BPF

# 1) 加载eBPF代码
b = BPF(src_file="open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="bcc_do_sys_openat2")

# 2) 输出头
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))

# 3) 定义性能事件打印函数
start = 0
def print_event(cpu, data, size):
    global start
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))

# 4) 绑定性能事件映射和输出函数,并从映射中循环读取数据
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()
  • 第 1) 处加载 eBPF 程序并挂载到内核探针上;
  • 第 2) 处输出一行 Header 字符串表示数据的格式;
  • 第 3) 处print_event 定义一个数据处理的回调函数,打印进程的名字、PID 以及它调用 openat 时打开的文件;
  • 第 4) 处则是使用poll读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe的内容,并回调print_event打印到标准输出中。
jian@ubuntu:~/Desktop/bpf/sec$ sudo python3 open.py
...
TIME(s)            COMM             PID    FILE            
0.000000000        b'vmtoolsd'      802    b'/proc/meminfo'
0.000071315        b'vmtoolsd'      802    b'/proc/vmstat' 
0.000133430        b'vmtoolsd'      802    b'/proc/stat'   
0.000187572        b'vmtoolsd'      802    b'/proc/zoneinfo'
0.000282536        b'vmtoolsd'      802    b'/proc/uptime' 
0.000294861        b'vmtoolsd'      802    b'/proc/diskstats'

相对于前面的 Hello World,它的输出不仅格式更为清晰,还把进程打开的文件名输出出来了,这在调试的时候尤其有用。

三、开发hash映射程序
刚刚使用事件映射已经满足我们平常的需要,但是如果我们需要同时跟踪几个函数,还想让跟踪点之间可以访问相互想要提交的数据,仅仅事件映射是不行的,这时候就需要使用哈希映射了。我们还是来个例子吧,我们追踪系统调用execve吧,那么他的进入追踪函数和返回函数分别是sys_enter_execve,sys_exit_execve;我们让他们共享data_t 数据,并且各自把数据填充进去,最后再一起提交。
同样,先看 eBPF 程序execsnoop.c的代码:

// 引入内核头文件
#include 
#include 
#include 

// consts for arguments (ensure below stack size limit 512)
#define ARGSIZE 64
#define TOTAL_MAX_ARGS 5
#define FULL_MAX_ARGS_ARR (TOTAL_MAX_ARGS * ARGSIZE)
#define LAST_ARG (FULL_MAX_ARGS_ARR - ARGSIZE)

// perf event map (sharing data to userspace) and hash map (sharing data between tracepoints)
struct data_t {
    u32 pid;
    char comm[TASK_COMM_LEN];
    int retval;
    unsigned int args_size;
    char argv[FULL_MAX_ARGS_ARR];
};
BPF_PERF_OUTPUT(events);
BPF_HASH(tasks, u32, struct data_t);

// helper function to read string from userspace.
static int __bpf_read_arg_str(struct data_t *data, const char *ptr)
{
	if (data->args_size > LAST_ARG) {
		return -1;
	}

	int ret = bpf_probe_read_user_str(&data->argv[data->args_size], ARGSIZE,
					  (void *)ptr);
	if (ret > ARGSIZE || ret < 0) {
		return -1;
	}
	// increase the args size. the first tailing '\0' is not counted and hence it
	// would be overwritten by the next call.
	data->args_size += (ret - 1);

	return 0;
}

//定义sys_enter_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_enter_execve)
{
    // 变量定义
    unsigned int ret = 0;
    const char **argv = (const char **)(args->argv);

    // 获取进程PID和进程名称
    struct data_t data = { };
    u32 pid = bpf_get_current_pid_tgid();
    data.pid = pid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));

    // 获取第一个参数(即可执行文件的名字)
    if (__bpf_read_arg_str(&data, (const char *)argv[0]) < 0) {
        goto out;
    }

    // 获取其他参数(限定最多5个)
	for (int i = 1; i < TOTAL_MAX_ARGS; i++) {
		if (__bpf_read_arg_str(&data, (const char *)argv[i]) < 0) {
			goto out;
		}
	}

 out:
    // 存储到哈希映射中
    tasks.update(&pid, &data);
    return 0;
}

// 定义sys_exit_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_exit_execve)
{
    // 从哈希映射中查询进程基本信息
    u32 pid = bpf_get_current_pid_tgid();
    struct data_t *data = tasks.lookup(&pid);

    // 填充返回值并提交到性能事件映射中
    if (data != NULL) {
        data->retval = args->ret;
        events.perf_submit(args, data, sizeof(struct data_t));

        // 最后清理进程信息
        tasks.delete(&pid);
    }

    return 0;
}
  • struct data_t:定义了一个包含进程基本信息的数据结构,它将用在哈希映射的值中(其中的参数大小 args_size
    会在读取参数内容的时候用到);
  • BPF_PERF_OUTPUT(events) : 定义了一个性能事件映射;
  • BPF_HASH(tasks,u32, struct data_t) : 定义了一个哈希映射,其键(tasks)为 32 位的进程PID,而值则是进程基本信息 data_t。
  • tasks.update(&pid, &data):把进程的基本信息存储到哈希映射中。
  • tasks.lookup(&pid):从哈希映射中通过进程ID获取进程基本信息 data_t。
  • tasks.delete(&pid):从哈希映射中通过进程ID删除这个映射。

再看execsnoop.py:


# 引入库函数
from bcc import BPF
from bcc.utils import printb

# 1) 加载eBPF代码
b = BPF(src_file="execsnoop.c")

# 2) print header
print("%-6s %-16s %-3s %s" % ("PID", "COMM", "RET", "ARGS"))

# 3) 定义性能事件打印函数
def print_event(cpu, data, size):
    # BCC自动根据"struct data_t"生成数据结构
    event = b["events"].event(data)
    printb(b"%-6d %-16s %-3d %-16s" % (event.pid, event.comm, event.retval, event.argv))

# 4) 绑定性能事件映射和输出函数,并从映射中循环读取数据
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

最后通过 Python 运行,并在另一个终端中执行 ls 命令,你就可以得到如下的输出:

jian@ubuntu:~/share/bpf/third$ sudo python3 execsnoop.py
PID    COMM             RET ARGS
6178   bash             0   ls--color=auto  

使用BCC开发eBPF程序就讲到这里了,其实BCC中自带了很多eBPF程序,我们也可以直接那来使用的。

你可能感兴趣的:(学习,linux)