1. 前言

上篇文章中我们为文件 open系统调用采用了 perf_event 的方式将数据从内核上报至用户程序。但是到目前为止,我们只是实现了文件打开记录的跟踪,并没有对文件访问的结果是成功还是失败进行展示。

kprobe 相对应的 kretprobe 实现可以帮助我们获取到 sys_open 函数的返回值。为了拿到 sys_open 系统调用的详细信息和返回结果,我们需要在 sys_open 函数入口时将文件访问相关信息进行保存,而在函数返回时读取函数入口保存的信息将返回结果与文件详情一起合并,将数据发送至用户程序。

kprobe 方式的实现是基于函数入口处的跟踪机制,而 kretprobe 跟踪机制则可以实现对于返回结果的跟踪。由于入口与返回跟踪是两种机制,需要在两个函数中实现。在函数入口跟踪函数中以 bpf_get_current_pid_tgid() 为 key,将入口相关数据信息保存在BPF 提供的 HASH 结构中,在函数返回以 bpf_get_current_pid_tgid() 为 key 读取入口相关信息与结果值合并,形成上报的完整数据 。

整个程序的大体实现结构如下图所示:

sys_open_return_arch

2. 代码实现

2.1 BPF 跟踪程序

在 BPF 跟踪程序中我们以下调整:

  1. 增加保存缓存结果数据格式,并定义中间结果保存的 HASH_MAP 结构,调整内核 BPF 跟踪程序与用户程序通信的数据格式,增加 retcomm 字段;
  2. 调整跟踪函数的逻辑 trace_syscall_open ,将入口处的详细信息发送至用作中间缓存的 HASH_MAP 结构中;
  3. 新增加跟踪函数返回结果函数 trace_syscall_open_return ,读取函数返回结果,并读取 trace_syscall_open 保存的入口相关信息合并后使用 perf_submit 函数发送数据;

prog 相关代码调整后如下:

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
prog = """
#include <uapi/linux/limits.h> // for  NAME_MAX
#include <linux/sched.h>       // for TASK_COMM_LEN
struct event_data_t {
    u32 pid;
    u32 ret;                    // 1. sys_open 的返回结果
    char comm[TASK_COMM_LEN];   // 1. 增加当前进程的命令行
    char fname[NAME_MAX];
};
// 1. add 
struct val_t {
    u64 id;
    const char *fname;
};

BPF_HASH(infotmp, u64, struct val_t); // 1. 增加保存中间结果的 HASH_MAP
BPF_PERF_OUTPUT(open_events);

int trace_syscall_open(struct pt_regs *ctx, const char __user *filename, int flags) {
    struct val_t val = {};
    u64 id = bpf_get_current_pid_tgid();
  
    val.id = id;
    val.fname = filename;
  
    infotmp.update(&id, &val);  // 2. 保存中间结果至 hash_map 中
  
    return 0;
}

int trace_syscall_open_return(struct pt_regs *ctx) // 3.1 新增加 ret 跟踪函数
{
    u64 id = bpf_get_current_pid_tgid();
    struct val_t *valp;
    struct event_data_t evt = {};
  
    valp = infotmp.lookup(&id); // 3.2 从 hash_map 中获取到  sys_open 函数保存的中间数据
    if (valp == 0) {
        // missed entry
        return 0;
    }
    evt.pid = id >> 32;
    evt.ret = PT_REGS_RC(ctx); // 3.3 读取结果值
    bpf_probe_read(&evt.fname, sizeof(evt.fname), (void *)valp->fname);
    bpf_get_current_comm(&evt.comm, sizeof(evt.comm));  // 3.4 读取当前进程的 commandline 至结构体中
  
    open_events.perf_submit(ctx, &evt, sizeof(evt));
  
    infotmp.delete(&id);  // 3.4 完成跟踪删除中间结果
    return 0;
}
"""

对于代码的调整,这里我们详细进行解释:

  1. 主要增加功能对应的数据结构(或调整),并增加数据存储相关的定义。

    • 我们为结构体 event_data_t 增加了 retcomm 两个字段分别用于保存 sys_open 系统调用函数的结果和调用的进程名称;
    • 新增定义新增的中间结果数据结构 struct val_t,主要包含 idfname 两个字段,分别为 pid_tgid 和读取的文件名字;
    • BPF_HASH(infotmp, u64, struct val_t); 定义中间数据保存的 HASH_MAP,使用 BCC 宏定义,key 为 u64 类型,value 为 struct val_t 结构; map 结构为 BPF 程序重要的数据结构,可以在用户空间与内核空间同时访问,是通信的一种重要方式,除了 hash 类型还有 array 等多种数据类型;
  2. trace_syscall_open 函数中的调整主要是将当前信息保存至 HASH_MAP 中,infotmp.update(&id, &val); 以 id 为 key,将 val 对象结果保存至 infotmp 中;

  3. 新增加函数 trace_syscall_open_return 用于跟踪 sys_open 函数的结果;

    • valp = infotmp.lookup(&id); 用于读取在函数入口保存的信息,如果未查询到则直接返回,需要注意的是 lookup 函数的入参和出产都是指针类型,使用前需要判断;
    • evt.ret = PT_REGS_RC(ctx); 主要是使用宏 PT_REGS_RC 从 ctx 字段中读取本次函数跟踪的返回值;
    • bpf_get_current_comm(&evt.comm, sizeof(evt.comm)); 使用函数 bpf_get_current_comm 读取当前进程的 commandline 并保存至对应字段中 evt.comm
    • 在完成正常结果获取并将数据发送以后,需要清理当前的记录,调用 infotmp.delete(&id); 实现;

至此,BPF 程序的调整已经完成,由于我们新增了 kretprobe 方式跟踪,因此需要在用户空间增加对应的关联动作。

2.2 用户空间程序

1
2
3
4
5
6
b.attach_kretprobe(event=b.get_syscall_fnname("open"), fn_name="trace_syscall_open_return") # 1. add

# process event
def print_event(cpu, data, size):
  event = b["open_events"].event(data)
  print("[%s] %d, %s, res: %d"%(event.comm, event.pid, event.fname, event.ret))  # 2. print

用户空间程序的调整相对比较少,增加 kretprobe 跟踪,并在事件跟踪函数中打印出我们新增加的对应字段:

  1. b.attach_kretprobe 函数将将 sys_open 函数的结果跟踪与函数 trace_syscall_open_return 完成关联;
  2. 我们在 print_event 函数中增加新增结构体字段 commret 的打印;

函数运行的结果如下:

1
2
3
4
#./open_perf_output_ret.py
[CmsGoAgent.linu] 722, /proc/7744/cmdline, res: 5
[CmsGoAgent.linu] 722, /proc/7744/stat, res: 5
[CmsGoAgent.linu] 722, /proc/7744/status, res: 5

完整代码可以参见 open_perf_output_ret.py

3. 总结

支持我们通过 3 篇文章介绍了如何在 BPF 技术中使用 kprobe 技术跟踪 open 系统调用,并展示了如何使用 perf_event 的方式进行数据通信;最后我们使用 kretprobe 技术实现了 open 系统调用的返回值,并使用 BPF 技术中实现的 HASH_MAP 保存中间临时结果,并在 kretprobe对应的跟踪函数中将函数入口信息与结果值合并传递值用户空间。

基于上述案例,我们可以很容易扩展到其他相关的系统调用,并通过功能增强实现我们自己的 BPF 跟踪程序。

相关阅读: