最简单的eBPF程序 - Hello World
环境
操作系统 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 程序
安装好这些依赖库后,我们就可以编写代码了,分两部分:
- eBPF 程序
- 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
这样,当有新的进程产生时,就会输出一句日志。