伟明部落格

最简单的eBPF程序 - Hello World

--发布于 2024-10-07 21:37:04

环境

操作系统 Ubuntu 24.04.1 LTS

内核 6.8.0-45-generic

安装依赖

# 安装 clang 用来编译 eBPF 程序
sudo apt install clang
# 安装必要的头文件
sudo apt install linux-headers-$(uname -r)
# 安装 BPF 库
sudo apt install libbpf-dev

使用apt-file命令,我们可以看到 libbpf-dev 库安装了下列文件

root@zwm-VMware-Virtual-Platform:~# apt-file list libbpf-dev
libbpf-dev: /usr/include/bpf/bpf.h        
libbpf-dev: /usr/include/bpf/bpf_core_read.h
libbpf-dev: /usr/include/bpf/bpf_endian.h
libbpf-dev: /usr/include/bpf/bpf_helper_defs.h
libbpf-dev: /usr/include/bpf/bpf_helpers.h
libbpf-dev: /usr/include/bpf/bpf_tracing.h
libbpf-dev: /usr/include/bpf/btf.h
libbpf-dev: /usr/include/bpf/libbpf.h
libbpf-dev: /usr/include/bpf/libbpf_common.h
libbpf-dev: /usr/include/bpf/libbpf_legacy.h
libbpf-dev: /usr/include/bpf/libbpf_version.h
libbpf-dev: /usr/include/bpf/skel_internal.h
libbpf-dev: /usr/include/bpf/usdt.bpf.h
libbpf-dev: /usr/lib/x86_64-linux-gnu/libbpf.a
libbpf-dev: /usr/lib/x86_64-linux-gnu/libbpf.so
libbpf-dev: /usr/lib/x86_64-linux-gnu/pkgconfig/libbpf.pc
libbpf-dev: /usr/share/doc/libbpf-dev/changelog.Debian.gz
libbpf-dev: /usr/share/doc/libbpf-dev/copyright

有对应的头文件和库文件,可以重点阅读一下这些头文件。

编写 eBPF 程序

安装好这些依赖库后,我们就可以编写代码了,分两部分:

  1. eBPF 程序
  2. eBPF 加载器

其中,eBPF程序是实现业务逻辑的主体。例如,当检测到execve系统调用跟踪点被执行时,将触发这些业务逻辑代码的运行。而eBPF加载器的作用是将我们编写的eBPF程序加载到内核中。因为加载eBPF程序的动作是有一套流程的,因此,这个eBPF加载器的代码也是比较通用的。

eBPF 程序

下面这个ebpf_program.c就是eBPF程序的逻辑代码。当系统调用execve()被触发时,就会调用detect_execve()这个函数。这个函数的操作就是输出字符串execve called

/* 
 * ebpf_program.c 
 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("tracepoint/syscalls/sys_enter_execve")
int detect_execve(struct bpf_raw_tracepoint_args *ctx)
{
    bpf_printk("%s\n", "execve called");
    return 0;
}

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

编译

clang -O2 -target bpf -I /usr/include/x86_64-linux-gnu -c ebpf_program.c -o ebpf_program.o

如果报找不到头文件的错误,可以使用clang-I指令,将对应的头文件目录包含进来即可。

新版本内核的系统可以忽略下面这段

注意:在旧版本的内核中(如5.15.0-112-generic),可能不支持自定义 .rodata段,程序会崩溃core dumped,如下

root@vultr:~/lab/ebpf/hello# ./ebpf_loader 
libbpf: elf: skipping unrecognized data section(5) .rodata.str1.1
libbpf: prog 'detect_execve': bad map relo against '.rodata.str1.1' in section '.rodata.str1.1'
Segmentation fault (core dumped)

需要将这一行

bpf_printk("%s\n", "execve called");

替换为如下的代码

char msg[] = "execve called";
bpf_printk("%s\n", msg);

替换后,虽然运行时也有警告,但不会崩溃。

eBPF 加载器

下面这个ebpf_loader.c将上一步生成的 eBPF程序 ebpf_program.o 加载到内核中,当对应的事件触发时,eBPF程序就会输出一句话execve called到文件/sys/kernel/debug/tracing/trace_pipe中。eBPF加载器调用read_trace_pipe()函数从/sys/kernel/debug/tracing/trace_pipe循环读取日志,输出到终端中。

/*
 * ebpf_loader.c
 */
#include <linux/bpf.h>
#include <bpf/libbpf.h>

/* 下面这三个头文件是 read_trace_pipe() 函数需要的 */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

/* 日志输出 DEBUGFS */
#define TRACE_PIPE "/sys/kernel/debug/tracing/trace_pipe" 

/* read trace logs from debug fs */
void read_trace_pipe(void)
{
    int trace_fd;

    trace_fd = open(TRACE_PIPE, O_RDONLY, 0);
    if (trace_fd < 0)
        return;

    /* 循环读取,直到程序结束 */
    while (1) {
        static char buf[4096];
        ssize_t sz;

        sz = read(trace_fd, buf, sizeof(buf) - 1);
        if (sz> 0) {
            buf[sz] = 0;
            puts(buf);
        }
    }
}

int main() {
    /* 这个文件就是上一步产生 eBPF 程序的目标文件 */
    const char *filepath = "ebpf_program.o"; 
    struct bpf_object *bpfObject;
    struct bpf_program *prog;
    struct bpf_link *link;
    int err;

    bpfObject = bpf_object__open_file(filepath, NULL);
    if (!bpfObject) {
        printf("Error!Failed to open %s\n", filepath);
        return -1;
    }

    err = bpf_object__load(bpfObject);
    if (err) {
        printf("Error!Failed to load %s\n", filepath);
        return -1;
    }

    /* 这个 detect_execve 就是上一步的 eBPF 程序的函数名 */
    prog = bpf_object__find_program_by_name(bpfObject, "detect_execve");
    if (!prog) {
        printf("Error!Failed to find eBPF program\n");
        return -1;
    }

    link = bpf_program__attach(prog);
    if (!link) {
        printf("Error!Failed to attach eBPF program\n");
        return -1;
    }

    /* 读取输出日志,并输出到终端 */
    read_trace_pipe();

    return 0;
}

编译

clang -O2 -o ebpf_loader ebpf_loader.c -lbpf

运行

sudo ./ebpf_loader

这样,当有新的进程产生时,就会输出一句日志。

参考资料

Tracing System Calls Using eBPF - Part 1

eBPF 入门之编程

libbpf: support custom .rodata.*/.data.* sections

--更新于 2024-10-07 21:39:30