skaiuijing

BPF程序类型

BPF程序分为两大类:跟踪和网络

在内核中,以下是各种程序的添加顺序:

套接字过滤器程序

该程序附加到socket处理的数据报上,访问对应的数据报,但只能观察,不能修改内容。

程序类型:BPF_PROG_TYPE_SOCKET_FILTER

kprobe程序

动态附加到内核调用点的函数,可以通过指定函数调用来附加程序。

程序类型:BPF_PROG_TYPE_KPRBE

tracepoint跟踪点程序

可以附加到内核提供的跟踪点处理程序上。系统中的所有跟踪点都定义在/sys/kernel/debug/tracing/events 目录中 , 你可
以找到包括跟踪点的每个子系统,并将BPF程序附加在其上。同时,BPF也有自己的跟踪点,可以编写BPF程序检查其他BPF程序的
行为。

程序类型:BPF_PROG_TYPE_TRACEPOINT

XDP程序

针对数据报控制的操作,用于决定如何处理数据包,XDP程序返回XDP_PASS表示成功传递到内核下一个子系统。

程序类型:BPF_PROG_TYPE_XDP

Perf事件程序

Perf是内核的内部分析器,可以产生硬件和软件的性能数据事件。Perf事件程序将BPF代码附加到Perf事件上。每次Perf产生分析数据时,程序代码将被执行。

程序类型:BPF_PROG_TYPE_PERF_EVENT

cgroup套接字程序

cgroup是内核中对进程组控制的组件,包括CPU使用率、内存大小等控制。cgroup套接字程序可以将BPF逻辑附加到cgroup上,允许在cgroup包含的进程组中控制网络流量。网络数据包在传入cgroyp控制的进程前,会触发程序。

程序类型:BPF_PROG_TYPE_CGROUP_SKB

cgroup打开套接字程序

该程序类型允许cgroup内的进程组中任何进程打开网络套接字时执行代码。

程序类型:BPF_PROG_TYPE_CGROUP_SOCK

套接字选项程序

当数据包通过内核网络协议栈的多个阶段中转时,该类型程序允许运行时修改套接字连接选项,该程序附加到cgroup上。

程序类型:BPF_PROG_TYPE_SOCK_OPS

注:应用:

Facebook使用此功能自行设计RTO时间。RTO是TCP协议栈中的重传恢复时间,在TCP中会通过RTT估计器计算,但Facebook使用BPF实现了自定义。

套接字映射程序

cgroup设备程序

套接字消息传递程序

原始跟踪点程序

cgroup套接字地址程序

套接字重用端口程序

流量解析程序

其他BPF程序

BPF类型格式

BTF(BPF Type Format)是一种专为 eBPF 程序和映射设计的元数据格式,用于在 ELF 文件中嵌入 C 语言类型和调试信息。它最初用于描述数据结构类型,后来扩展到包含函数签名和行号信息,使得 eBPF 程序可以在 不同内核版本间一次编译、多处运行(CO-RE)且能安全地访问内核数据结构。

BTF 的组成
BTF 数据通常分为两部分:

.BTF 节 存放类型定义和字符串表,包含结构体、枚举、数组、指针等类型信息。

.BTF.ext 节 存放函数原型信息和源码行号信息,用于调试、JIT 输出和 verifier 日志的可读化。

主要作用
类型安全访问 eBPF 程序可借助 BTF 信息,通过 BPF_CORE_READ() 等宏按字段名读取内核结构体,避免硬编码偏移。

跨版本兼容

程序编译时不依赖特定内核头,仅需从目标内核加载 BTF,即可在多种内核上运行。

调试与审计 bpftool、JIT 编译器和 verifier 都可利用 BTF 打印结构化类型和行号,极大提升可观测性。

使用bpftool工具,可以从当前内核中导出BTF为C头文件,也就是vmlinux.h,我们可以直接引用该头文件来获取数据结构信息而不用依靠重复引用内核头文件。

BPF尾调用

ctx全局指针及BPF_KPROBE宏

1. ctx 全局指针

  • 含义

    • ctx 是传入 eBPF kprobe 钩子函数的唯一参数,类型为 struct pt_regs *
    • 内核在触发 kprobe 时,会将当前寄存器状态保存在 pt_regs 结构中,供后续读取。
  • 主要字段(x86-64 ABI)

  • 参数提取

    • 使用宏 PT_REGS_PARMn(ctx) 获取第 n 个入参:

      1
      2
      int dfd = (int)PT_REGS_PARM1(ctx);                 // RDI → do_unlinkat 第 1 参数
      struct filename *name = (void *)PT_REGS_PARM2(ctx); // RSI → 第 2 参数
    • 如果要访问指针指向的数据,必须再用 bpf_probe_read() 或 CO-RE 宏读取内核内存。

2. BPF_KPROBE 宏

  • 作用

    • 将你的 C 函数自动“挂载”到指定内核函数的入口(kprobe)。
    • 自动从 ctx 中拆寄存器并将值以普通参数形式传入,省去手写 PT_REGS_PARMn() 的麻烦。
  • 用法

    1
    2
    3
    4
    5
    SEC("kprobe/do_unlinkat")
    int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name) {
    // dfd、name 已由宏自动提取
    return 0;
    }
  • 宏展开原理

    1. 跳板函数

      1
      2
      3
      4
      5
      int do_unlinkat(struct pt_regs *ctx) {
      return ____do_unlinkat(ctx,
      (int)PT_REGS_PARM1(ctx),
      (struct filename *)PT_REGS_PARM2(ctx));
      }
    2. 真正逻辑

      1
      2
      3
      4
      5
      static __always_inline int
      ____do_unlinkat(struct pt_regs *ctx, int dfd, struct filename *name) {
      // 用户写的业务代码
      return 0;
      }
    3. 最终 ELF 会在 kprobe/do_unlinkat 段下保存跳板符号,loader 加载后即挂载到内核函数入口。

3. 典型示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/sched.h>
#include <bpf/bpf_helpers.h>

SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name) {
char path[256];

// 从内核安全读出文件名
bpf_probe_read_str(path, sizeof(path), name->name);

bpf_printk("unlinkat: dfd=%d path=%s\n", dfd, path);
return 0;
}

4. 小结

  • ctx 保存了触发时的寄存器快照,必须通过受限 API(PT_REGS_PARMn + bpf_probe_read*)安全读取。
  • BPF_KPROBE 是 libbpf 提供的语法糖,自动完成寄存器拆解和节名声明,让 kprobe 逻辑更清晰简洁。

代码

实现信号追踪

内核空间

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/sched.h>
#include <trace/events/signal.h>

// 临时保存发送者信息:PID、comm、信号号
struct send_info_t {
u32 sender_pid;
char sender_comm[16];
u32 sig;
};

// 最终提交给用户态的完整事件
struct event_t {
u32 sender_pid;
char sender_comm[16];
u32 target_pid;
char target_comm[16];
u32 sig;
int result;
};

// Map:在 generate→deliver 之间传递发送者信息
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u64);
__type(value, struct send_info_t);
} send_map SEC(".maps");

// Ring Buffer Map:用于向用户态提交合并后的事件
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 16);
} ringbuf SEC(".maps");

// 捕获信号生成阶段
SEC("tracepoint/signal/signal_generate")
int on_signal_generate(struct trace_event_raw_signal_generate *ctx) {
u64 key = (u64)ctx->info;
struct send_info_t info = {};

info.sender_pid = ctx->pid;
bpf_get_current_comm(&info.sender_comm, sizeof(info.sender_comm));
info.sig = ctx->sig;

bpf_map_update_elem(&send_map, &key, &info, BPF_ANY);
return 0;
}

// 捕获信号投递阶段
SEC("tracepoint/signal/signal_deliver")
int on_signal_deliver(struct trace_event_raw_signal_deliver *ctx) {
u64 key = (u64)ctx->info;
struct send_info_t *s = bpf_map_lookup_elem(&send_map, &key);
if (!s)
return 0;

// 在 ringbuf 中申请一块内存
struct event_t *evt = bpf_ringbuf_reserve(&ringbuf, sizeof(*evt), 0);
if (!evt) {
// 申请失败,删除 Map 条目后返回
bpf_map_delete_elem(&send_map, &key);
return 0;
}

// 填充发送者信息
evt->sender_pid = s->sender_pid;
__builtin_memcpy(evt->sender_comm, s->sender_comm,
sizeof(evt->sender_comm));

// 填充接收者信息
evt->target_pid = ctx->pid;
bpf_get_current_comm(&evt->target_comm,
sizeof(evt->target_comm));

evt->sig = s->sig;
evt->result = 0;

// 提交到用户态
bpf_ringbuf_submit(evt, 0);

// 清理 Map
bpf_map_delete_elem(&send_map, &key);
return 0;
}

char LICENSE[] SEC("license") = "GPL";

用户空间:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "signal_trace_ringbuf.skel.h"

using namespace std;

// 与内核 event_t 保持一致
struct event_t {
uint32_t sender_pid;
char sender_comm[16];
uint32_t target_pid;
char target_comm[16];
uint32_t sig;
int32_t result;
};

// 从 /proc/<pid>/cmdline 读取完整命令行
static string get_cmdline(pid_t pid) {
string path = "/proc/" + to_string(pid) + "/cmdline";
ifstream ifs(path, ios::in | ios::binary);
if (!ifs)
return "";
string data((istreambuf_iterator<char>(ifs)),
istreambuf_iterator<char>());
// cmdline 以 '\0' 分隔参数,替换为 ' '
for (auto &c : data)
if (c == '\0')
c = ' ';
return data;
}

// Ring-buffer 回调:打印事件
static int handle_event(void *ctx, void *data, size_t len) {
auto *evt = static_cast<event_t *>(data);
string cmd = get_cmdline(evt->sender_pid);
if (cmd.empty())
cmd = string(evt->sender_comm);

cout << "SIG" << evt->sig << " "
<< cmd << "(" << evt->sender_pid << ") → "
<< evt->target_comm << "(" << evt->target_pid << ")"
<< "\n";
return 0;
}

int main() {
// 提升 rlimit,加载 BPF 对象
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
setrlimit(RLIMIT_MEMLOCK, &r);

// 打开并加载 BPF 程序
auto skel = signal_trace_ringbuf_skel__open();
if (!skel) {
cerr << "Failed to open BPF skeleton\n";
return 1;
}
if (signal_trace_ringbuf_skel__load(skel)) {
cerr << "Failed to load BPF program\n";
return 1;
}
if (signal_trace_ringbuf_skel__attach(skel)) {
cerr << "Failed to attach BPF programs\n";
return 1;
}

// 构建 ring buffer
int map_fd = bpf_map__fd(skel->maps.ringbuf);
struct ring_buffer *rb = ring_buffer__new(map_fd, handle_event, nullptr);
if (!rb) {
cerr << "Failed to create ring buffer\n";
return 1;
}

cout << "Tracing signals (with ringbuf)... Ctrl-C to exit.\n";
while (true) {
int err = ring_buffer__poll(rb, 100 /* ms */);
if (err < 0) {
cerr << "Error polling ringbuf: " << err << "\n";
break;
}
}

ring_buffer__free(rb);
signal_trace_ringbuf_skel__destroy(skel);
return 0;
}