本文是 eBPF 系列的第二篇文章,我们来学习 eBPF BCC 框架的进阶用法,对上一篇文章中的代码进行升级,动态输出进程运行时的参数情况。

主要内容包括:

  1. 通过 kprobe 挂载内核事件的 eBPF 程序要如何编写?
  2. 通过 tracepoint 挂载内核事件的 eBPF 程序要如何编写?
  3. eBPF 的程序事件类型有哪些?

在开始之前,我们来回顾一下前一篇文章的内容。

前一篇文章介绍了如何通过 BCC 框架来编写一个简单的 eBPF 程序。在内核空间,使用 c 程序实现 eBPF 的核心逻辑;在用户空间,使用 python 脚本作为 eBPF 程序的控制、加载和展示。其中,内核态通过若干 eBPF helper 函数,获取内核观测数据,并通过 PERF 区域,将这些数据传递到用户空间;用户态使用attach_kprobe() 将内核 eBPF 函数绑定到某个内核事件上。

整个流程如下图所示:

在上面的实现过程中,用户态通过 kprobe 的方式,为某个内核事件挂载自定义处理逻辑(图中是指定了内核中 do_execve 函数)。通过这种方式,我们能够监测绝大部分的内核函数,这正是 eBPF 技术牛逼的原因。

对于这种 kprobe 类型的 eBPF 程序,我们再来看一个例子(改编自 Brendan Gregg 大神的 execsnoop 工具:https://github.com/iovisor/bcc/blob/master/tools/execsnoop.py

1 进程执行参数的监控

接下来,我们要对上图中的工具再次进行功能升级,我希望这个工具在运行时,能够输出当前执行进程的参数信息。

如果将 eBPF 程序等同于 C 程序来看,这个问题似乎没那么困难。何以见得?

1.1 分析

sys_execve 系统调用的函数签名为:int execve(const char *filename, char *const argv[], char *const envp[]), 其中,argv[]便记录了进程执行的参数。我们大可以像提取 filename 的方式那样,提取 argv[],并将其传入到用户空间中。

但实际上,eBPF 程序与 C 程序并不等同。eBPF 编程中有 “两座大山” 般的限制,分别是:

限制一:eBPF 程序运行栈仅有 512 字节。

限制二:eBPF 程序可以调用的接口极其有限。

因此,如果我们想尝试在 512 字节的 eBPF 运行栈中完整拼接整理不定长的 argv[] 参数列表,是根本不可能的。

基于以上分析,本文给出一个比较合理的解决方案:

Q:如何防止运行栈爆栈?

1)既然运行栈有大小限制,不如直接将拼接操作转移到用户态完成。eBPF 程序只需要将 argv[] 数组中每个 argv 传输到用户态程序中。

2)对于长度过长的 argv,没办法了,只能手动截断了。

Q:用户态何时进行参数拼接?何时进行参数展示?

1)既然需要用户态完成拼接,那么,可以分为两个阶段。STEP-1,仅专注字符串的拼接;STEP-2,仅专注字符串展示。

2)对于 execve 系统调用,我们可以在 enter 时执行 STEP-1 操作,在 exit 是执行 STEP-2 操作。

接下来更新代码。

1.2 定义

首先,对于用于交互的结构体,增加两个个字段,其一用于记录 execve 调用的每个参数,其二用于记录 eBPF 执行的阶段;同时,去掉冗余字段 fname

#define ARGSIZE     128
#define MAXARG 60 enum event_step {
STEP_1, // STEP 1: 执行 argv 拼接
STEP_2, // STEP 2: 执行 argv 展示
}; struct data_t {
u32 pid;
enum event_step step; // 记录 eBPF 执行阶段
char comm[TASK_COMM_LEN];
char argv[ARGSIZE]; // 记录每一个参数
};

定义 BPF_PERF_OUTPUT

BPF_PERF_OUTPUT(events);

1.3 处理

实现 execve 系统调用 enterexit 回调函数:

// exter execve
int syscall__execve(struct pt_regs *ctx, const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) {
struct data_t data = {};
// 设置 step = STEP 1
data.step = STEP_1;
// 设置 pid
data.pid = bpf_get_current_pid_tgid() >> 32;
// 设置 comm
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// 设置每一个 argv,并导出
... return 0;
} // exit execve
int do_ret_sys_execve(struct pt_regs *ctx) {
struct data_t data = {};
// 设置 step = STEP 1
data.step = STEP_2;
// 设置 pid
data.pid = bpf_get_current_pid_tgid() >> 32;
// 设置 comm
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// 提交 perf
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}

注意,这里 bpf_get_current_pid_tgid() 辅助函数返回值高 32 为内核视角下的 process ID(用户视角下为 TID),低 32 位为内核视角下的 thread group ID(用户视角下的 PID)。这里右移 32 位,是获取用户视角的 PID。

1.4 绑定

用户态绑定 kprobe 事件:

b = BPF(src_file="execsnoop.c")
execve_fnname = b.get_syscall_fnname("execve")
# enter 事件
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
# exit 事件
b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve")

1.5 难点

内核态如何设置并导出每一个 argv[]

// 字符串提交
static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
// 提交 perf 之前,需要拷贝到用户态变量中
bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
// 将这个 argv 提交
events.perf_submit(ctx, data, sizeof(struct data_t));
return 1;
}
// 字符串控制
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
const char *argp = NULL;
bpf_probe_read_user(&argp, sizeof(argp), ptr);
// 是否到达末尾字符串
if (argp) {
return __submit_arg(ctx, (void *)(argp), data);
}
return 0;
} int syscall__execve(struct pt_regs *ctx, const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) {
// 设置过程
...
// (A) 设置每一个 argv,并导出
#pragma unroll
for (int i = 1; i < MAXARG; i++) {
if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
goto out;
}
// (B) 如果当前的 argv[] 太长了,进行截断操作
char ellipsis[] = "...";
__submit_arg(ctx, (void *)ellipsis, &data);
out:
return 0;
}

关注核心的两个步骤:

(A) MAXARG 代表一个 argv[] 的最大监测数量。首先要遍历这个 argv[] 的每一个字符串,如果这个字符不为 NULL(说明没有到当前 argv[] 结尾)或不超过最大值 MAXARG,那么将每个字符串提交到 PERF 区域。

注意:

低版本(5.3 以前)的 eBPF 程序不支持循环。5.3 版本后也仅支持有界循环。在低版本的 eBPF 中使用循环有一个小技巧,那就是通过 #pragma unroll 进行编译器循环展开预处理。

(B) 如果超过了这个最大数量 MAXARG,后面及时再有参数,也进行截断处理。

1.6 拼接

用户态获取和拼接参数列表是基于 eBPF 阶段的。

from collections import defaultdict

argv = defaultdict(list)
class EventStep(object):
STEP_1 = 0
STEP_2 = 1 # PERF 事件回调处理
def print_event(cpu, data, size):
event = b["events"].event(data)
# STEP 1:拼接
if event.step == EventStep.STEP_1:
argv[event.pid].append(event.argv)
# STEP 2:显示
elif event.step == EventStep.STEP_2:
argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
printb(b"%-16s %-7d %s" % (event.comm, event.pid, argv_text))
try:
del(argv[event.pid])
except Exception:
pass # 绑定 PERF 事件回调处理
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

用户态程序需要注意:event 事件通过 PERF 获取的结构数据为 Byte 类型,需要通过 decode('utf-8')/encode()str 类型进行转换。

1.7 完整代码和运行效果

// execsnoop.c
#include <linux/sched.h>
#include <linux/fs.h> #define ARGSIZE 128
#define MAXARG 60
enum event_step {
STEP_1,
STEP_2,
};
struct data_t {
u32 pid;
enum event_step step;
char comm[TASK_COMM_LEN];
char argv[ARGSIZE];
};
BPF_PERF_OUTPUT(events); static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
events.perf_submit(ctx, data, sizeof(struct data_t));
return 1;
}
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
const char *argp = NULL;
bpf_probe_read_user(&argp, sizeof(argp), ptr);
if (argp) {
return __submit_arg(ctx, (void *)(argp), data);
}
return 0;
}
// exter execve
int syscall__execve(struct pt_regs *ctx, const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) {
struct data_t data = {};
data.step = STEP_1;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
#pragma unroll
for (int i = 1; i < MAXARG; i++) {
if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
goto out;
}
char ellipsis[] = "...";
__submit_arg(ctx, (void *)ellipsis, &data);
out:
return 0;
}
// exit execve
int do_ret_sys_execve(struct pt_regs *ctx) {
struct data_t data = {};
data.step = STEP_2;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
# execsnoop.py
#!/usr/bin/python3
from bcc import BPF
from bcc.utils import printb
from collections import defaultdict argv = defaultdict(list)
class EventStep(object):
STEP_1 = 0
STEP_2 = 1 b = BPF(src_file="execsnoop.c")
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve") print("%-7s %-16s %s" % ("PID", "PCOMM", "ARGS")) # process event
def print_event(cpu, data, size):
event = b["events"].event(data)
fname = ""
if event.step == EventStep.STEP_1:
argv[event.pid].append(event.argv)
elif event.step == EventStep.STEP_2:
argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
printb(b"%-7d %-16s %s" % (event.pid, event.comm, argv_text))
try:
del(argv[event.pid])
except Exception:
pass # loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

运行效果:

2 Tracepoint 追踪点

前文提到过,kprobe 方式,几乎可以使 eBPF 挂载到内核中任意一个函数事件上,随着内核函数的执行而触发。但是,由于不同的内核版本,其某个具体函数的定义、参数和实现可能会有所不同(kprobe 实现的事件处理函数要求和挂载点函数拥有相同的参数)。因此,使用 kprobe 方式实现的 eBPF 程序可能无法在其他内核的主机上运行。此外,kprobe 无法挂载到静态函数或内联函数上。而出于性能考虑,大部分网络相关的内层函数都是内联或者静态的,因此,kprobe 方式在这些领域也只能望洋兴叹了。

上述两点,均为 kprobe 方式的局限性,它并不具备很好的可移植性。于是,从 Linux 内核 4.7 开始,能让 eBPF 使用的 tracepoint 出现了(官方文档)。tracepoint 是由内核开发人员在代码中设置的静态 hook 点,具有稳定的 API 接口,不会随着内核版本的变化而变化。但由于 tracepoint 是需要内核研发人员参数编写,其数量有限,并不是所有的内核函数中都具有类似的跟踪点,所以从灵活性上不如 kprobes 这种方式。

2.1 kprobe 和 tracepoint 对比

在 3.10 内核中,kprobetracepoint 方式对比如下:

内容 kprobe tracepoint
追踪类型 动态 静态
Hook 点数量 100000+ 1200+
稳定的 API

可以使用以下命令查看系统支持的 tracepoint,支持 grep 检索。

perf list
perf list | grep execve

上面的执行结果可以看到,execve系统调用具有两个 syscalls 类型的静态跟踪点,并且,tracepoint 已经对 enter 和 exit 做了区分,其功能基本等同于 kprobe/kretprobe

在使用 tracepoint 之前,我们需要了解 tracepoint 相关参数的格式。syscalls:sys_enter_execve 格式定义在 /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format 文件中。

# 查看 syscalls:sys_enter_execve 参数
cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format

2.2 重构代码

接下来,使用 tracepoint 方式重构第 1 节的代码,如下:

// execsnoop.c
#include <linux/sched.h>
#include <linux/fs.h> #define ARGSIZE 128
#define MAXARG 60 enum event_step {
STEP_1,
STEP_2,
};
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
enum event_step step;
char argv[ARGSIZE];
};
BPF_PERF_OUTPUT(events);
static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
bpf_probe_read_user(data->argv, sizeof(data->argv), ptr);
events.perf_submit(ctx, data, sizeof(struct data_t));
return 1;
}
static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) {
const char *argp = NULL;
bpf_probe_read_user(&argp, sizeof(argp), ptr);
if (argp) {
return __submit_arg(ctx, (void *)(argp), data);
}
return 0;
}
// (A) sys_enter_execve tracepoint
TRACEPOINT_PROBE(syscalls, sys_enter_execve) {
struct data_t data = {};
const char **argv = (const char **) (args->argv); data.step = STEP_1;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm)); #pragma unroll
for (int i = 1; i < MAXARG; i++) {
// (B) args 强制转换为 ctx
if (submit_arg((struct pt_regs *)args, (void *)&argv[i], &data) == 0)
goto out;
}
char ellipsis[] = "...";
__submit_arg((struct pt_regs *)args, (void *)ellipsis, &data);
out:
return 0;
}
// sys_exit_execve tracepoint
TRACEPOINT_PROBE(syscalls, sys_exit_execve) {
struct data_t data = {};
data.step = STEP_2;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm)); events.perf_submit(args, &data, sizeof(data));
return 0;
}
# execsnoop.py
#!/usr/bin/python3
from bcc import BPF
from bcc.utils import printb
from collections import defaultdict argv = defaultdict(list)
class EventStep(object):
STEP_1 = 0
STEP_2 = 1 # (C) 不再通过 kprobe 绑定
b = BPF(src_file="execsnoop.c")
print("%-7s %-16s %s" % ("PID", "PCOMM", "ARGS")) # process event
def print_event(cpu, data, size):
event = b["events"].event(data)
if event.step == EventStep.STEP_1:
argv[event.pid].append(event.argv)
elif event.step == EventStep.STEP_2:
argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
printb(b"%-7d %-16s %s" % (event.pid, event.comm, argv_text))
try:
del(argv[event.pid])
except Exception:
pass # loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

注意:

A)一个 tracepoint 定义接收两个参数,TRACEPOINT_PROBE(syscalls, sys_enter_execve) 第一个为子系统名称,第二个为事件名称。

B)tracepoint 中的所有参数都会包含在一个固定名称的 args 的结构体中。args 类型为 struct tracepoint__syscalls__sys_enter_open,其第一个字段为 u64 __do_not_use__;,该字段为 ctx 的保留位置。因此,args 可以被强制转换为 ctx

ctx 是啥?

在《Linux 内核观测技术 BPF》一书中,ctx被称为“上下文”,提供了访问内核正在处理的信息。我们可以通过 PT_REGS_RC(ctx) 来获取当前函数的返回值。

C)用户态代码不再需要 attach_kprobe 手动绑定。

3 eBPF 程序事件类型

像是 kprobetracepoint 将 eBPF 程序挂载到内核事件的方式,可以暂且被称为 eBPF 事件类型。事实上,除了以上列出的两种,eBPF 事件类型还有很多,选取其中一些列举如下:

  • kprobes/kretprobes:内核函数事件。不再赘述。
  • tracepoint:内核跟踪点事件。不再赘述。
  • uprobes/uretprobes:用户空间函数事件,可以绑定监听一个用户空间的函数。
  • USDT probes:用户自定义的静态追踪点。用户可以在用户空间的程序中插入静态追踪点,用于挂载 eBPF。
  • LSM Probes:LSM Hook 挂载点。需要内核版本 5.7 以上。

由于篇幅限制,不再列举其他 eBPF 事件类型了,后面如果有精力,再补一篇文章。

4 总结

本文在前一篇文章的基础上,对进程执行监控工具(execsnoop)进行了升级,实时打印进程执行时传入的参数列表;并通过 kprobetracepoint 两种方式,绑定 eBPF 程序,给出了代码实现。同时,对这两种 eBPF 事件类型进行了简单比较。显然,在你手动开发一个 eBPF 程序时,建议使用 tracepoint,以追求更好的稳定性和可移植性。文章的最后,简单列出了一些支持的 eBPF 事件类型。

以上抛砖引玉,如有不正确指出,请大家及时斧正。如果你喜欢这篇文章,请点个推荐吧!

【eBPF-02】入门:基于 BCC 框架的程序进阶的更多相关文章

  1. 文件系统(02):基于SpringBoot框架,管理Xml和CSV文件类型

    本文源码:GitHub·点这里 || GitEE·点这里 一.文档类型简介 1.XML文档 XML是可扩展标记语言,是一种用于标记电子文件使其具有结构性的标记语言.标记指计算机所能理解的信息符号,通过 ...

  2. VS2010/MFC编程入门之四(MFC应用程序框架分析)

    VS2010/MFC编程入门之四(MFC应用程序框架分析)-软件开发-鸡啄米 http://www.jizhuomi.com/software/145.html   上一讲鸡啄米讲的是VS2010应用 ...

  3. 基于Struts2框架实现登录案例 之 程序国际化

    国际化牵涉的知识非常多,这里只能简单的介绍,程序国际化的一般做法是:在jsp页面时, 不是直接输出信息,而是输出一个key值,该key值在不同语言环境下找到对应资源文件下的 对应信息,因此首先要创建满 ...

  4. 基于express框架的应用程序骨架生成器介绍

    作者:zhanhailiang 日期:2014-11-09 本文将介绍怎样使用express-generator工具高速生成基于express框架的应用程序骨架: 1. 安装express-gener ...

  5. 基于ABP框架的SignalR,使用Winform程序进行功能测试

    在ABP框架里面,默认会带入SignalR消息处理技术,它同时也是ABP框架里面实时消息处理.事件/通知处理的一个实现方式,SignalR消息处理本身就是一个实时很好的处理方案,我在之前在我的Winf ...

  6. 基于Dubbo框架构建分布式服务

    Dubbo是Alibaba开源的分布式服务框架,我们可以非常容易地通过Dubbo来构建分布式服务,并根据自己实际业务应用场景来选择合适的集群容错模式,这个对于很多应用都是迫切希望的,只需要通过简单的配 ...

  7. 【翻译】首个基于NHibernate的应用程序

    首个基于NHibernate的应用程序  Your first NHibernate based application 英文原文地址:http://www.nhforge.org/wikis/how ...

  8. 快速入门系列--WebAPI--03框架你值得拥有

    接下来进入的是俺在ASP.NET学习中最重要的WebAPI部分,在现在流行的互联网场景下,WebAPI可以和HTML5.单页应用程序SPA等技术和理念很好的结合在一起.所谓ASP.NET WebAPI ...

  9. VS2010/MFC编程入门之三(VS2010应用程序工程中文件的组成结构)

    VS2010/MFC编程入门之三(VS2010应用程序工程中文件的组成结构)-软件开发-鸡啄米 http://www.jizhuomi.com/software/143.html   鸡啄米在上一讲中 ...

  10. [转载] 基于Dubbo框架构建分布式服务

    转载自http://shiyanjun.cn/archives/1075.html Dubbo是Alibaba开源的分布式服务框架,我们可以非常容易地通过Dubbo来构建分布式服务,并根据自己实际业务 ...

随机推荐

  1. KRPANO资源分析工具下载网展全景图

    示:目前分析工具中的全景图下载功能将被极速全景图下载大师替代,相比分析工具,极速全景图下载大师支持更多的网站(包括各类KRPano全景网站,和百度街景) 详细可以查看如下的链接: 极速全景图下载大师官 ...

  2. 「atcoder - 214G」Three Permutations

    la traduction. link. 如果我们对于每一个 \(k\in[0,n]\) 找到所有满足存在 \(k\) 个 \(i\) 使得 \(r_i=p_i\) 或 \(r_i=q_i\) 条件的 ...

  3. Solution -「CF 1039D」You Are Given a Tree

    Description Link. 有一棵 \(n\) 个节点的树,其中一个简单路径的集合被称为 \(k\) 合法当且仅当:树的每个节点至多属于其中一条路径,且每条路径恰好包含 \(k\) 个点. 对 ...

  4. elmentui表单重置初始值问题与解决方法

    背景 在做管理台项目时,我们会经常使用到表单+表格+弹窗表单的组合,以完成对数据的增.删.查.改. 在vue2+elementui项目中,使用弹窗dialog+表单form,实现对数据的添加和修改. ...

  5. 2023-10-07:用go语言,给定n个二维坐标,表示在二维平面的n个点, 坐标为double类型,精度最多小数点后两位, 希望在二维平面上画一个圆,圈住其中的k个点,其他的n-k个点都要在圆外。

    2023-10-07:用go语言,给定n个二维坐标,表示在二维平面的n个点, 坐标为double类型,精度最多小数点后两位, 希望在二维平面上画一个圆,圈住其中的k个点,其他的n-k个点都要在圆外. ...

  6. answer answerdev/answer:latest 开源问答平台

    freedidi.com/10294.html   https://youtu.be/A2GUgvPlTBE?si=jdhqXL1WttLrLgiQ     docker依赖 yum install ...

  7. CF1534C

    题目简化和分析: 涉及算法:并查集. 为什么要使用并查集: 因为交换只能是列交换,并且保证不与别的重复 我们通过观察题目发现,某些列之间互为限制关系 即如果某列序列排序方式固定,则被限制的列也为固定的 ...

  8. 【Unity3D】Shader Graph节点

    1 前言 ​ Shader Graph 16.0.3 中有 208 个 Node(节点),本文梳理了 Shader Graph 中大部分 Node 的释义,官方介绍详见→Node-Library. ​ ...

  9. 记一次有趣的 buffer overflow detected 问题分析

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 环境说明   无 前言   在我开发的一个实验和学习库中,在很久 ...

  10. 一步步带你剖析Java中的Reader类

    本文分享自华为云社区<深入理解Java中的Reader类:一步步剖析>,作者:bug菌. 前言 在Java开发过程中,我们经常需要读取文件中的数据,而数据的读取需要一个合适的类进行处理.J ...