Skip to content

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_OPENSSLTLS 数据缓冲区大小(16KB)kern/common.h:39
MAX_DATA_SIZE_MYSQLMySQL 查询缓冲区大小(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

关键特性:

  1. 调试打印: debug_bpf_printk() 宏根据 DEBUG_PRINT 标志条件编译调试输出。这避免了生产构建中的性能开销。

  2. 缓冲区大小常量: 符合 RFC 的不同协议大小(TLS 最大片段 = 16KB,数据库查询限制)。

  3. 全局变量: 标记为 volatile 以表明它们是从用户空间通过常量编辑器初始化的。

来源: kern/common.h:15-86


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 中定义了两个关键过滤函数:

c
// 检查 PID/UID 是否应被拒绝
static __inline bool filter_rejects(u32 pid, u32 uid)

// 检查事件是否通过所有过滤器
static __always_inline bool passes_filter(struct pt_regs *ctx)

这些函数检查 target_pidtarget_uid 全局变量以实现选择性跟踪。

来源: kern/ecapture.h:1-130


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

为什么使用这种模式?

  1. 函数参数仅在入口时有效
  2. 返回值和缓冲区内容仅在返回时有效
  3. 线程 ID(bpf_get_current_pid_tgid())关联入口和返回

来源: kern/openssl.h:268-323, kern/openssl.h:331-351


事件结构定义

事件结构定义了从内核发送到用户空间的数据格式。所有事件结构都是紧凑的,并在 eBPF 程序文件的顶部声明。

SSL 数据事件

TLS 明文捕获的主要事件:

c
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 连接生命周期:

c
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 密钥提取:

c
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 核心都有一个专用的环形缓冲区。

c
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。类似的定义还有:

用于上下文存储的 HASH 映射

在探针入口和返回之间存储临时数据:

c
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 字节堆栈限制:

c
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

将网络连接映射到进程上下文:

c
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 字节的堆栈限制。大型结构必须通过映射分配:

模式实现:

c
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

数据读取辅助函数

读取用户空间内存:

c
bpf_probe_read_user(&dest, sizeof(dest), src_ptr);

用于从目标应用程序的内存空间读取数据。示例位于 kern/openssl.h:186

读取内核内存:

c
bpf_probe_read_kernel(&dest, sizeof(dest), kernel_ptr);

用于读取内核结构,如 struct sock。示例位于 kern/openssl.h:409

CO-RE 字段访问:

c
#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 模式下提供可移植的字段访问。

过滤模式

探针开始时的标准过滤检查:

c
if (!passes_filter(ctx)) {
    return 0;
}

检查 target_pidtarget_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-351SSL_read/write 钩子
连接跟踪kern/openssl.h:354-525Kprobe 实现
SSL FD 映射kern/openssl.h:528-543SSL_set_fd 钩子

典型程序流程

  1. 包含依赖项: 标准头文件(ecapture.htc.h
  2. 定义事件类型: 枚举和常量
  3. 声明事件结构: 用于用户空间通信的紧凑结构
  4. 声明 BPF 映射: 事件、上下文和大缓冲区的存储
  5. 实现辅助函数: 可重用的数据提取逻辑
  6. 实现探针: 带有 SEC() 注解的入口/返回探针对
  7. 实现 Kprobe: 用于网络上下文的内核级跟踪

来源: kern/openssl.h:1-544


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: 直接指针运算,使用硬编码的偏移量(由偏移脚本生成)

兼容性宏:

c
#ifdef asm_inline
#undef asm_inline
#define asm_inline asm
#endif

位于 kern/ecapture.h:43-46,处理 CLANG 与内核 asm_inline 宏的不兼容性。

来源: kern/ecapture.h:18-88


总结

eCapture 中的 eBPF 程序遵循一致的结构:

  1. 模块化头文件: 通用定义分离到可重用的头文件中
  2. 事件驱动: 通过 PERF_EVENT_ARRAY 映射发送结构化事件
  3. 两阶段探针: 入口探针存储上下文,返回探针读取结果
  4. 堆栈解决方案: 通过 PERCPU_ARRAY 映射分配大型结构
  5. 双重编译: 支持 CO-RE(可移植)和 non-CO-RE(兼容)模式
  6. 过滤: 全局变量启用基于 PID/UID 的选择性跟踪
  7. 网络上下文: Kprobe 为 TC 分类器构建进程到连接的映射

所有程序都包含全面的错误处理,带有调试打印语句和映射查找的空值检查。该架构通过辅助函数最大化代码重用,同时保持不同捕获模块之间的清晰分离。

eBPF 程序结构 has loaded