C++ eBPF (Extended Berkeley Packet Filter):在内核中运行安全程序

好的,各位技术同仁,大家好!今天咱们来聊聊一个有点酷炫,又有点神秘的技术——C++ eBPF,也就是在内核中运行安全程序。别害怕,听起来高大上,其实拆开来看,也没那么复杂。我会尽量用大白话,加上一些代码示例,让大家都能理解。

eBPF:内核世界的瑞士军刀

首先,eBPF到底是个啥玩意?你可以把它想象成一个在Linux内核里运行的“虚拟机”,但这个“虚拟机”非常安全,而且性能极高。它允许你在内核中插入一些小程序(用特定的方式编译),来观察、修改内核的行为,而无需修改内核源码,也不需要重启内核。

想象一下,以前你想了解网络流量,可能要用tcpdump抓包,或者写内核模块。tcpdump会影响性能,内核模块又太危险,一不小心就可能搞崩系统。有了eBPF,你就可以在内核里安全地、高效地做这些事情。

所以,eBPF就像一把瑞士军刀,网络监控、安全分析、性能分析、甚至是应用层的trace,它都能胜任。

为什么是C++?

eBPF本身是用一个类汇编的语言(BPF bytecode)编写的,但直接写bytecode太痛苦了。所以,通常我们会用高级语言(比如C、C++)来编写eBPF程序,然后用编译器把它编译成BPF bytecode。

C++的优势在于:

  • 性能: C++编译出来的代码通常比其他高级语言更快,这对于在内核中运行的程序来说至关重要。
  • 控制力: C++允许你更精细地控制内存和资源,这对于编写高效的eBPF程序很有帮助。
  • 库支持: 有一些C++库(比如libbpf、bcc)可以简化eBPF程序的开发。

eBPF程序的基本结构

一个典型的eBPF程序通常包含以下几个部分:

  1. 用户空间程序: 负责加载、管理和与eBPF程序交互。
  2. eBPF程序: 这是实际运行在内核中的代码,负责完成特定的任务。
  3. Map(映射): 用于在用户空间程序和eBPF程序之间共享数据。

代码示例:Hello, eBPF!

我们先来写一个最简单的eBPF程序,它会在内核中打印一条消息。

1. eBPF程序 (hello.bpf.c):

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <stdio.h>

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
    char msg[] = "Hello, eBPF from C++!n";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}

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

这段代码做了什么:

  • #include <linux/bpf.h>#include <bpf/bpf_helpers.h>: 引入eBPF相关的头文件。
  • SEC("tracepoint/syscalls/sys_enter_execve"): 这是一个宏,用于指定eBPF程序的类型和挂载点。这里表示我们要挂载到 execve 系统调用的入口点。
  • int bpf_prog(void *ctx): 这是eBPF程序的入口函数。ctx 是上下文指针,包含了关于触发事件的信息。
  • bpf_trace_printk(msg, sizeof(msg)): 这是一个eBPF提供的辅助函数,用于在内核中打印消息。注意,它只能打印到内核的trace pipe中,不能直接打印到终端。
  • char LICENSE[] SEC("license") = "GPL";: eBPF程序必须声明许可证,否则无法加载。

2. 用户空间程序 (loader.cpp):

#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/bpf.h>
#include <bpf/libbpf.h>

int main() {
    // 加载eBPF程序
    struct bpf_object *obj = bpf_obj_get("hello.bpf.o");
    if (!obj) {
        std::cerr << "Failed to open BPF object" << std::endl;
        return 1;
    }

    // 加载并验证eBPF程序
    int err = bpf_prog_load(obj->progs[0]);
    if (err) {
        std::cerr << "Failed to load BPF program: " << err << std::endl;
        return 1;
    }

    // 附加eBPF程序到tracepoint
    int prog_fd = bpf_program__fd(obj->progs[0]);
    if (prog_fd < 0) {
        std::cerr << "Failed to get program FD" << std::endl;
        return 1;
    }

    int tp_fd = open("/sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/id", O_RDONLY);
    if (tp_fd < 0) {
        std::cerr << "Failed to open tracepoint ID file" << std::endl;
        return 1;
    }

    char buf[32];
    ssize_t len = read(tp_fd, buf, sizeof(buf) - 1);
    close(tp_fd);
    if (len <= 0) {
        std::cerr << "Failed to read tracepoint ID" << std::endl;
        return 1;
    }
    buf[len] = '';
    int tp_id = std::stoi(buf);

    char attach_path[256];
    snprintf(attach_path, sizeof(attach_path),
             "/sys/fs/bpf/prog_attach/%d", tp_id);

    int attach_fd = open(attach_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (attach_fd < 0) {
        std::cerr << "Failed to open attach file: " << attach_path << std::endl;
        return 1;
    }

    if (write(attach_fd, &prog_fd, sizeof(prog_fd)) != sizeof(prog_fd)) {
        std::cerr << "Failed to attach program" << std::endl;
        close(attach_fd);
        return 1;
    }
    close(attach_fd);

    std::cout << "eBPF program attached!  Run some commands to trigger execve." << std::endl;
    std::cout << "Check /sys/kernel/debug/tracing/trace_pipe for output." << std::endl;

    // 等待一段时间,然后卸载程序
    sleep(10);

    // 卸载eBPF程序 (Simplified Unloading - Requires root and proper setup)
    //  This part needs to be adapted based on how attach_path was constructed and system configuration.
    //  The following is a placeholder for demonstration purposes and SHOULD NOT be used directly in production
    //  without careful consideration.

    // Remove the attachment - Requires Root Privileges
    /*
    if (remove(attach_path) != 0) {
        std::cerr << "Failed to detach program. Manual cleanup might be needed." << std::endl;
        perror("remove"); // Print detailed error if any
    } else {
        std::cout << "eBPF program detached." << std::endl;
    }
    */

    bpf_object__close(obj);  // Clean up the bpf object

    return 0;
}

这个C++程序做了以下事情:

  • 加载eBPF程序: 使用 bpf_obj_get 函数从编译后的目标文件(hello.bpf.o)加载eBPF程序。
  • 加载并验证eBPF程序: 使用 bpf_prog_load 函数将eBPF程序加载到内核并进行验证。
  • 附加eBPF程序到tracepoint: 找到tracepoint对应的ID,构造attach path,然后将eBPF程序的fd写入attach path。
  • 等待一段时间: 为了让你有时间去触发 execve 系统调用,并查看输出。
  • 卸载eBPF程序: 移除tracepoint attachment,并关闭bpf object。 注意: 这部分需要root权限,并且需要根据实际系统配置进行调整。 卸载程序需要谨慎处理,以免影响系统稳定性。

3. 编译和运行

首先,你需要安装eBPF相关的工具和库(比如libbpf、bcc)。具体安装方法可以参考相关文档。

然后,按照以下步骤编译和运行:

# 编译eBPF程序
clang -target bpf -D__TARGET_ARCH_x86_64 -O2 -Wall -c hello.bpf.c -o hello.bpf.o

# 编译用户空间程序
g++ loader.cpp -o loader -lbpf

# 运行用户空间程序 (需要root权限)
sudo ./loader

运行 loader 后,它会将eBPF程序加载到内核,并附加到 execve 系统调用的入口点。

接下来,你可以运行一些命令(比如 lspwd),来触发 execve 系统调用。

最后,你可以通过以下命令查看内核的trace pipe:

sudo cat /sys/kernel/debug/tracing/trace_pipe

你应该能看到类似这样的输出:

Hello, eBPF from C++!

恭喜你,你的第一个C++ eBPF程序成功运行了!

Map(映射)的应用

Map是eBPF程序和用户空间程序之间共享数据的关键机制。它可以存储各种类型的数据,比如计数器、直方图、甚至是更复杂的数据结构。

我们来修改一下上面的例子,使用Map来统计 execve 系统调用的次数。

1. eBPF程序 (counter.bpf.c):

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(long long));
    __uint(max_entries, 1);
} counter_map SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
    int key = 0;
    long long *count = bpf_map_lookup_elem(&counter_map, &key);
    if (count) {
        (*count)++;
    }
    return 0;
}

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

这段代码做了什么:

  • 定义了一个名为 counter_map 的Map,类型为 BPF_MAP_TYPE_ARRAY,key的类型为 int,value的类型为 long long,最多包含一个元素。
  • bpf_prog 函数中,首先通过 bpf_map_lookup_elem 函数查找Map中key为0的元素。
  • 如果找到了,就将该元素的值加1。

2. 用户空间程序 (counter.cpp):

#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/bpf.h>
#include <bpf/libbpf.h>

int main() {
    // 加载eBPF程序
    struct bpf_object *obj = bpf_obj_get("counter.bpf.o");
    if (!obj) {
        std::cerr << "Failed to open BPF object" << std::endl;
        return 1;
    }

    // 加载并验证eBPF程序
    int err = bpf_prog_load(obj->progs[0]);
    if (err) {
        std::cerr << "Failed to load BPF program: " << err << std::endl;
        return 1;
    }

    // 附加eBPF程序到tracepoint
    int prog_fd = bpf_program__fd(obj->progs[0]);
    if (prog_fd < 0) {
        std::cerr << "Failed to get program FD" << std::endl;
        return 1;
    }

    int tp_fd = open("/sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/id", O_RDONLY);
    if (tp_fd < 0) {
        std::cerr << "Failed to open tracepoint ID file" << std::endl;
        return 1;
    }

    char buf[32];
    ssize_t len = read(tp_fd, buf, sizeof(buf) - 1);
    close(tp_fd);
    if (len <= 0) {
        std::cerr << "Failed to read tracepoint ID" << std::endl;
        return 1;
    }
    buf[len] = '';
    int tp_id = std::stoi(buf);

    char attach_path[256];
    snprintf(attach_path, sizeof(attach_path),
             "/sys/fs/bpf/prog_attach/%d", tp_id);

    int attach_fd = open(attach_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (attach_fd < 0) {
        std::cerr << "Failed to open attach file: " << attach_path << std::endl;
        return 1;
    }

    if (write(attach_fd, &prog_fd, sizeof(prog_fd)) != sizeof(prog_fd)) {
        std::cerr << "Failed to attach program" << std::endl;
        close(attach_fd);
        return 1;
    }
    close(attach_fd);

    std::cout << "eBPF program attached!  Run some commands to trigger execve." << std::endl;

    // 获取Map的fd
    int map_fd = bpf_object__find_map_fd_by_name(obj, "counter_map");
    if (map_fd < 0) {
        std::cerr << "Failed to find map FD" << std::endl;
        return 1;
    }

    // 等待一段时间,然后读取Map中的值
    sleep(10);

    int key = 0;
    long long count = 0;
    if (bpf_map_lookup_elem(map_fd, &key, &count) == 0) {
        std::cout << "execve count: " << count << std::endl;
    } else {
        std::cerr << "Failed to lookup map element" << std::endl;
    }

    // 卸载eBPF程序 (Simplified Unloading - Requires root and proper setup)
    /*
    if (remove(attach_path) != 0) {
        std::cerr << "Failed to detach program. Manual cleanup might be needed." << std::endl;
        perror("remove"); // Print detailed error if any
    } else {
        std::cout << "eBPF program detached." << std::endl;
    }
    */

    bpf_object__close(obj);

    return 0;
}

这段代码做了什么:

  • 获取Map的fd: 使用 bpf_object__find_map_fd_by_name 函数根据Map的名称找到对应的fd。
  • 读取Map中的值: 使用 bpf_map_lookup_elem 函数从Map中读取key为0的元素的值。

3. 编译和运行

# 编译eBPF程序
clang -target bpf -D__TARGET_ARCH_x86_64 -O2 -Wall -c counter.bpf.c -o counter.bpf.o

# 编译用户空间程序
g++ counter.cpp -o counter -lbpf

# 运行用户空间程序 (需要root权限)
sudo ./counter

运行 counter 后,它会将eBPF程序加载到内核,并附加到 execve 系统调用的入口点。

接下来,你可以运行一些命令(比如 lspwd),来触发 execve 系统调用。

最后,你应该能看到类似这样的输出:

execve count: 123

其中,123execve 系统调用的次数。

一些更高级的应用场景

eBPF的应用场景非常广泛,这里列举一些常见的例子:

  • 网络监控: 监控网络流量、分析网络性能、实现DDoS防御。
  • 安全分析: 检测恶意代码、分析系统调用、实现入侵检测。
  • 性能分析: 跟踪函数调用、分析CPU使用率、优化程序性能。
  • 应用层的trace: 跟踪用户请求、分析延迟、诊断问题。

eBPF的挑战和注意事项

虽然eBPF很强大,但也存在一些挑战和注意事项:

  • 安全性: eBPF程序运行在内核中,如果编写不当,可能会导致安全问题。因此,eBPF程序需要经过严格的验证。
  • 性能: eBPF程序的性能非常重要,因为它会直接影响系统的性能。因此,需要仔细优化eBPF程序。
  • 复杂性: eBPF程序的开发相对复杂,需要掌握一定的内核知识。
  • 兼容性: 不同版本的内核可能对eBPF的支持有所不同,需要注意兼容性问题。
  • 权限: 加载和管理eBPF程序通常需要root权限。

总结

C++ eBPF是一项非常强大的技术,它允许你在内核中安全地、高效地运行程序,从而实现各种各样的功能。虽然eBPF的学习曲线可能比较陡峭,但只要掌握了基本概念和工具,就能发挥它的巨大潜力。

希望今天的分享对大家有所帮助。谢谢大家!

表格:eBPF程序开发常用工具

工具名称 描述
libbpf 用于加载、验证和管理eBPF程序的C库
BCC (BPF Compiler Collection) 一套用于创建eBPF程序的工具,包括编译器、库和示例
bpftool 用于检查、调试和管理eBPF程序的命令行工具
perf Linux自带的性能分析工具,可以与eBPF集成

表格:eBPF Map类型

Map类型 描述
BPF_MAP_TYPE_HASH 哈希表,适用于存储键值对
BPF_MAP_TYPE_ARRAY 数组,适用于存储固定大小的数据
BPF_MAP_TYPE_PERCPU_HASH 每个CPU一个哈希表
BPF_MAP_TYPE_PERCPU_ARRAY 每个CPU一个数组
BPF_MAP_TYPE_LRU_HASH 最近最少使用(LRU)哈希表
BPF_MAP_TYPE_RINGBUF 环形缓冲区,适用于高效的数据传输

记住,学习eBPF是一个持续探索的过程。 祝大家在eBPF的世界里玩得开心!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注