eBPF 程序结构
本页面记录了 eCapture 代码库中 eBPF 程序的组织和结构,包括通用头文件、探针类型定义、事件结构、BPF 映射和辅助函数。它为编写或修改 eBPF 程序的开发人员提供参考。
有关为特定库版本生成结构体偏移量的信息,请参见结构体偏移量计算。有关实现完整捕获模块的指南,请参见添加新模块。
目录组织
所有 eBPF 内核空间程序都位于 kern/ 目录中。这些程序遵循模块化结构,具有共享头文件和模块特定的实现文件。
来源: kern/common.h, kern/ecapture.h, kern/openssl.h, kern/tc.h
通用头文件
common.h - 全局定义
common.h 头文件定义了所有 eBPF 程序中使用的常量、宏和全局变量。
| 定义 | 用途 | 行引用 |
|---|---|---|
debug_bpf_printk() | 条件调试输出宏 | kern/common.h:19-26 |
TASK_COMM_LEN | 进程名称长度(16 字节) | kern/common.h:28 |
MAX_DATA_SIZE_OPENSSL | TLS 数据缓冲区大小(16KB) | kern/common.h:39 |
MAX_DATA_SIZE_MYSQL | MySQL 查询缓冲区大小(256B) | kern/common.h:40 |
AF_INET, AF_INET6 | 地址族常量 | kern/common.h:49-50 |
target_pid, target_uid | 全局过滤变量(volatile) | kern/common.h:68-69 |
less52 | 内核版本标志(< 5.2) | kern/common.h:66 |
关键特性:
调试打印:
debug_bpf_printk()宏根据DEBUG_PRINT标志条件编译调试输出。这避免了生产构建中的性能开销。缓冲区大小常量: 符合 RFC 的不同协议大小(TLS 最大片段 = 16KB,数据库查询限制)。
全局变量: 标记为
volatile以表明它们是从用户空间通过常量编辑器初始化的。
ecapture.h - 核心基础设施
ecapture.h 头文件为所有 eBPF 程序提供基础,处理 CO-RE(一次编译,到处运行)与 non-CO-RE 编译路径。
条件编译:
预处理器指令 #ifndef NOCORE 位于 kern/ecapture.h:18,决定使用哪个路径:
- CO-RE 模式: 使用
vmlinux.h中的 BTF(BPF 类型格式)进行内核结构定义。需要内核 >= 5.2 并支持 BTF。 - Non-CO-RE 模式: 手动包含内核头文件。兼容旧内核但可移植性较差。
过滤函数:
在 kern/ecapture.h:93-127 中定义了两个关键过滤函数:
// 检查 PID/UID 是否应被拒绝
static __inline bool filter_rejects(u32 pid, u32 uid)
// 检查事件是否通过所有过滤器
static __always_inline bool passes_filter(struct pt_regs *ctx)这些函数检查 target_pid 和 target_uid 全局变量以实现选择性跟踪。
SEC() 宏与探针类型
eBPF 程序使用 SEC() 宏声明它们的附加类型。节名称决定了程序如何加载和附加。
探针类型参考
| 节名称 | 探针类型 | 用途 | 示例函数 |
|---|---|---|---|
uprobe/<func> | 用户空间入口探针 | 钩住函数入口 | kern/openssl.h:331 |
uretprobe/<func> | 用户空间返回探针 | 钩住函数返回 | kern/openssl.h:336 |
kprobe/<func> | 内核入口探针 | 钩住内核函数入口 | kern/openssl.h:374 |
kretprobe/<func> | 内核返回探针 | 钩住内核函数返回 | kern/openssl.h:456 |
classifier | 流量控制分类器 | 网络数据包过滤 | kern/tc.h:274 |
探针模式:入口/返回配对
大多数 uprobe 钩子遵循两阶段模式:
入口探针(probe_entry_SSL_write):
- 在执行前捕获函数参数
- 将上下文存储在以线程 ID 为键的 BPF 映射中
- 示例:kern/openssl.h:331-334
返回探针(probe_ret_SSL_write):
- 从 BPF 映射中检索存储的上下文
- 在函数完成后读取实际数据
- 向用户空间发送事件
- 示例:kern/openssl.h:336-339
为什么使用这种模式?
- 函数参数仅在入口时有效
- 返回值和缓冲区内容仅在返回时有效
- 线程 ID(
bpf_get_current_pid_tgid())关联入口和返回
来源: kern/openssl.h:268-323, kern/openssl.h:331-351
事件结构定义
事件结构定义了从内核发送到用户空间的数据格式。所有事件结构都是紧凑的,并在 eBPF 程序文件的顶部声明。
SSL 数据事件
TLS 明文捕获的主要事件:
struct ssl_data_event_t {
enum ssl_data_event_type type; // kSSLRead 或 kSSLWrite
u64 timestamp_ns; // 纳秒时间戳
u32 pid; // 进程 ID
u32 tid; // 线程 ID
char data[MAX_DATA_SIZE_OPENSSL]; // 捕获的明文(16KB)
s32 data_len; // 实际数据长度
char comm[TASK_COMM_LEN]; // 进程名称
u32 fd; // 文件描述符
s32 version; // TLS 版本
u32 bio_type; // OpenSSL BIO 类型
};定义于 kern/openssl.h:28-39。
连接事件
跟踪 TCP 连接生命周期:
struct connect_event_t {
unsigned __int128 saddr; // 源地址(IPv4/IPv6)
unsigned __int128 daddr; // 目标地址
char comm[TASK_COMM_LEN]; // 进程名称
u64 timestamp_ns; // 事件时间戳
u64 sock; // 内核套接字指针
u32 pid; // 进程 ID
u32 tid; // 线程 ID
u32 fd; // 文件描述符
u16 family; // AF_INET 或 AF_INET6
u16 sport; // 源端口
u16 dport; // 目标端口
u8 is_destroy; // 连接关闭标志
u8 pad[7]; // 填充
} __attribute__((packed));定义于 kern/openssl.h:41-55。注意 __attribute__((packed)) 以消除填充空洞。
主密钥事件
用于 TLS 1.2 和 1.3 密钥提取:
struct mastersecret_t {
// TLS 1.2
s32 version;
u8 client_random[SSL3_RANDOM_SIZE];
u8 master_key[MASTER_SECRET_MAX_LEN];
// TLS 1.3
u32 cipher_id;
u8 early_secret[EVP_MAX_MD_SIZE];
u8 handshake_secret[EVP_MAX_MD_SIZE];
u8 handshake_traffic_hash[EVP_MAX_MD_SIZE];
u8 client_app_traffic_secret[EVP_MAX_MD_SIZE];
u8 server_app_traffic_secret[EVP_MAX_MD_SIZE];
u8 exporter_master_secret[EVP_MAX_MD_SIZE];
};定义于 kern/openssl_masterkey.h:25-39。包含 TLS 1.2(单个主密钥)与 TLS 1.3(多个流量密钥)的不同字段。
来源: kern/openssl.h:23-55, kern/openssl_masterkey.h:21-43
BPF 映射定义
BPF 映射在内核和用户空间之间提供存储和通信。映射使用 SEC(".maps") 注解声明。
映射类型参考
PERF_EVENT_ARRAY 映射
用于向用户空间发送数据。每个 CPU 核心都有一个专用的环形缓冲区。
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
__uint(max_entries, 1024);
} tls_events SEC(".maps");定义于 kern/openssl.h:79-84。类似的定义还有:
connect_events位于 kern/openssl.h:87-92mastersecret_events位于 kern/openssl_masterkey.h:48-53skb_events位于 kern/tc.h:57-62
用于上下文存储的 HASH 映射
在探针入口和返回之间存储临时数据:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); // 线程 ID
__type(value, struct active_ssl_buf); // 函数参数
__uint(max_entries, 1024);
} active_ssl_write_args_map SEC(".maps");定义于 kern/openssl.h:104-109。键始终是 bpf_get_current_pid_tgid() 用于线程关联。
用于大缓冲区的 PERCPU_ARRAY
通过使用 per-CPU 映射存储来规避 eBPF 的 512 字节堆栈限制:
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, u32);
__type(value, struct ssl_data_event_t); // 16KB+ 结构
__uint(max_entries, 1);
} data_buffer_heap SEC(".maps");定义于 kern/openssl.h:113-118。始终使用键 0 访问,为大型事件结构提供堆分配的存储。
用于网络上下文的 LRU_HASH
将网络连接映射到进程上下文:
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct net_id_t); // 5 元组:协议,源/目标 IP/端口
__type(value, struct net_ctx_t); // PID,UID,comm
__uint(max_entries, 10240);
} network_map SEC(".maps");定义于 kern/tc.h:72-77。LRU(最近最少使用)自动驱逐旧条目。
来源: kern/openssl.h:74-134, kern/tc.h:56-77, kern/openssl_masterkey.h:45-67
辅助函数模式
堆栈限制解决方案
eBPF 程序有 512 字节的堆栈限制。大型结构必须通过映射分配:
模式实现:
static __inline struct ssl_data_event_t* create_ssl_data_event(u64 current_pid_tgid) {
u32 kZero = 0;
struct ssl_data_event_t* event =
bpf_map_lookup_elem(&data_buffer_heap, &kZero);
if (event == NULL) {
return NULL;
}
// 初始化字段...
return event;
}定义于 kern/openssl.h:141-158。类似模式在 make_event() 中位于 kern/openssl_masterkey.h:71-78。
数据读取辅助函数
读取用户空间内存:
bpf_probe_read_user(&dest, sizeof(dest), src_ptr);用于从目标应用程序的内存空间读取数据。示例位于 kern/openssl.h:186。
读取内核内存:
bpf_probe_read_kernel(&dest, sizeof(dest), kernel_ptr);用于读取内核结构,如 struct sock。示例位于 kern/openssl.h:409。
CO-RE 字段访问:
#define READ_KERN(ptr) \
({ \
typeof(ptr) _val; \
__builtin_memset((void *)&_val, 0, sizeof(_val)); \
bpf_core_read((void *)&_val, sizeof(_val), &ptr); \
_val; \
})定义于 kern/tc.h:22-28。在 CO-RE 模式下提供可移植的字段访问。
过滤模式
探针开始时的标准过滤检查:
if (!passes_filter(ctx)) {
return 0;
}检查 target_pid 和 target_uid 与当前进程。示例位于 kern/openssl.h:269。
来源: kern/openssl.h:141-191, kern/tc.h:22-28, kern/ecapture.h:93-127
完整程序结构示例
OpenSSL TLS 捕获程序
文件结构分解
| 部分 | 行范围 | 用途 |
|---|---|---|
| 许可证与包含 | kern/openssl.h:1-16 | 头文件依赖 |
| 常量与枚举 | kern/openssl.h:19-26 | 事件类型,默认值 |
| 事件结构 | kern/openssl.h:28-72 | 数据格式定义 |
| BPF 映射 | kern/openssl.h:74-134 | 存储声明 |
| 辅助函数 | kern/openssl.h:136-323 | 共享逻辑 |
| Uprobe 入口点 | kern/openssl.h:325-351 | SSL_read/write 钩子 |
| 连接跟踪 | kern/openssl.h:354-525 | Kprobe 实现 |
| SSL FD 映射 | kern/openssl.h:528-543 | SSL_set_fd 钩子 |
典型程序流程
- 包含依赖项: 标准头文件(
ecapture.h,tc.h) - 定义事件类型: 枚举和常量
- 声明事件结构: 用于用户空间通信的紧凑结构
- 声明 BPF 映射: 事件、上下文和大缓冲区的存储
- 实现辅助函数: 可重用的数据提取逻辑
- 实现探针: 带有 SEC() 注解的入口/返回探针对
- 实现 Kprobe: 用于网络上下文的内核级跟踪
CO-RE 与 Non-CO-RE 差异
代码库支持两种编译模式,在构建时确定。
编译标志
| 模式 | 标志 | vmlinux.h | 内核头文件 | 可移植性 |
|---|---|---|---|---|
| CO-RE | (默认) | 必需 | 不需要 | 高 - 在任何 BTF 内核上运行 |
| Non-CO-RE | -D NOCORE | 不使用 | 必需 | 低 - 内核特定 |
预处理器条件
条件编译位于 kern/ecapture.h:18-88。
主要差异
类型定义:
- CO-RE: 使用
vmlinux.h中的 BTF(BPF 类型格式)定义所有内核类型 - Non-CO-RE: 手动包含内核头文件,可能定义简化的结构(例如,
struct tcphdr位于 kern/ecapture.h:69-72)
字段访问:
- CO-RE: 使用
bpf_core_read()进行可移植的字段访问,处理偏移差异 - Non-CO-RE: 直接指针运算,使用硬编码的偏移量(由偏移脚本生成)
兼容性宏:
#ifdef asm_inline
#undef asm_inline
#define asm_inline asm
#endif位于 kern/ecapture.h:43-46,处理 CLANG 与内核 asm_inline 宏的不兼容性。
总结
eCapture 中的 eBPF 程序遵循一致的结构:
- 模块化头文件: 通用定义分离到可重用的头文件中
- 事件驱动: 通过 PERF_EVENT_ARRAY 映射发送结构化事件
- 两阶段探针: 入口探针存储上下文,返回探针读取结果
- 堆栈解决方案: 通过 PERCPU_ARRAY 映射分配大型结构
- 双重编译: 支持 CO-RE(可移植)和 non-CO-RE(兼容)模式
- 过滤: 全局变量启用基于 PID/UID 的选择性跟踪
- 网络上下文: Kprobe 为 TC 分类器构建进程到连接的映射
所有程序都包含全面的错误处理,带有调试打印语句和映射查找的空值检查。该架构通过辅助函数最大化代码重用,同时保持不同捕获模块之间的清晰分离。