eunomia-bpf: A Dynamic Loading Framework for CO-RE eBPF program with WASM

Actions Status GitHub release (latest by date)

Introduction

eunomia-bpf is a dynamic loading library base on CO-RE libbpf, and a compile toolchain. With eunomia-bpf, you can:

  • Write eBPF kernel code only and No code generation, we will automatically exposing your data from kernel
  • Compile eBPF kernel code to a JSON, you can dynamically load it on another machine without recompile
  • Package, distribute, and run user-space and kernel-space eBPF programs together in OCI compatible WASM module
  • very small and simple! The library itself <1MB and no LLVM/Clang dependence, can be embedded easily in you project
  • as fast as <100ms and little resource need to dynamically load and run eBPF program

With eunomia-bpf, you can also get pre-compiled eBPF programs running from the cloud to the kernel in 1 line of bash, kernel version and architecture independent! For example:

$ sudo ecli run sigsnoop

Base on eunomia-bpf, we have an eBPF pacakge manager in LMP project, with OCI images and ORAS. Powered by WASM, an eBPF program may be able to:

  • have isolation and protection for operating system resources, both user-space and kernel-space
  • safely execute user-defined or community-contributed eBPF code as plug-ins in a software product
  • Write eBPF programs with the language you favor, distribute and run the programs on another kernel or arch.

We have tested on x86 and arm platform, more Architecture tests will be added soon.

eunomia-bpf

eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。

使用 eunomia-bpf ,可以:

  • 在编写 eBPF 程序或工具时只编写 libbpf 内核态代码,自动获取内核态导出信息;
  • 使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;
  • eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。

eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpf, libbpfgo, libbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等。

项目地址

Install

use binary

To install ecli, just download and use the binary:

$ # download the release from https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecli
$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ecli

You can also found exporter and other tools in github.com/eunomia-bpf/eunomia-bpf/releases

build from source

please refer to:documents/build.md

common problem

if you get a error like /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version GLIBCXX_3.4.29 not found on old version kernels,

try:

sudo apt-get upgrade libstdc++6

see https://stackoverflow.com/questions/65349875/where-can-i-find-glibcxx-3-4-29

Quick Start

Github Template

Use this as a github action, to compile online or as an template:https://github.com/eunomia-bpf/ebpm-template

Online Experience

You can use the online experience service provided by bolipi, compile online, run online, and obtain visualization results online:https://bolipi.com/ebpf/home/online

Hello World

Create a new project:

$ mkdir hello
$ cd hello

Create a new hello.bpf.c in the hello folder with the following content:

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

typedef int pid_t;

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

SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
 pid_t pid = bpf_get_current_pid_tgid() >> 32;
 bpf_printk("BPF triggered from PID %d.\n", pid);
 return 0;
}

Assuming the parent directory of the hello folder is /path/to/repo, the next steps:

$ # download ecli binary
$ wget https://aka.pw/bpf-ecli -O ./ecli && chmod +x ./ecli
$ # use docker to compile the ebpf code to a file `package.json`
$ docker run -it -v /path/to/repo/hello:/src yunwei37/ebpm:latest
$ # run eBPF program
$ sudo ./ecli run package.json

When using docker, you need to mount the directory containing the .bpf.c file to the /src directory of the container, and there is only one .bpf.c file in the directory;

It keeps track of the pids of all processes making the write system call:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
cat-42755   [003] d...1 48755.529860: bpf_trace_printk: BPF triggered from PID 42755.
             cat-42755   [003] d...1 48755.529874: bpf_trace_printk: BPF triggered from PID 42755.

Our compiled eBPF code can also be adapted to multiple kernel versions. You can directly copy package.json to another machine, and then run it directly without recompiling (CO-RE: Compile Once Run Every Where). package.json can be transmitted and distributed over the network, usually, the compressed version is only a few kb to tens of kb.

eunomia-bpf 用户手册: 让 eBPF 程序的开发和部署尽可能简单

传统来说, eBPF 的开发方式主要有 BCC、libbpf 等方式。要完成一个 BPF 二进制程序的开发,需要搭建开发编译环境,要关注目标系统的内核版本情况,需要掌握从 BPF 内核态到用户态程序的编写,以及如何加载、绑定至对应的 HOOK 点等待事件触发,最后再对输出的日志及数据进行处理。

我们希望有这样一种 eBPF 的编译和运行工具链,就像其他很多语言一样:

  • 大多数用户只需要关注 bpf.c 程序本身的编写,不需要写任何其他的什么 Python, Clang 之类的用户态辅助代码框架; 这样我们可以很方便地分发、重用 eBPF 程序本身,而不需要和某种或几种语言的生态绑定;

  • 最大程度上和主流的 libbpf 框架实现兼容,原先使用 libbpf 框架编写的代码几乎不需要改动即可移植;eunomia-bpf 编写的 eBPF 程序也可以使用 libbpf 框架来直接编译运行;

  • 本地只需要下载一个很小的二进制运行时,没有任何的 Clang LLVM 之类的大型依赖,可以支持热插拔、热更新; 也可以作为 Lua 虚拟机那样的小模块直接编译嵌入其他的大型软件中,提供 eBPF 程序本身的服务;运行和启动时资源占用率都很低;

  • 让 eBPF 程序的分发和使用像网页和 Web 服务一样自然(Make eBPF as a service): 支持在集群环境中直接通过一次请求进行分发和热更新,仅需数十 kB 的 payload, <100ms 的更新时间,和少量的 CPU 内存占用即可完成 eBPF 程序的分发、部署和更新; 不需要执行额外的编译过程,就能得到 CO-RE 的运行效率;

C 语言 的 Hello World 开始

还记得您第一次写 C 语言Hello World 程序 吗?首先,我们需要一个 .c 文件,它包含一个 main 函数:

int main(void)
{
    printf("Hello, World!\n");
    return 0;
}

我们叫它 hello.c,接下来就只需要这几个步骤就好:

# if you are using Ubuntu without a c compiler
sudo apt insalll build-essentials
# compile the program
gcc -o hello hello.c
# run the program
./hello

只需要写一个 c 文件,执行两行命令就可以运行;大多数情况下你也可以把编译好的可执行文件直接移动到其他同样架构的机器或不同版本的操作系统上,然后运行它,也会得到一样的结果:

Hello World!

eunomia-bpf 的 Hello World

首先,我们需要一个 bpf.c 文件,它就是正常的、合法的 C 语言代码,和 libbpf 所使用的完全相同:

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

typedef int pid_t;

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

SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
 pid_t pid = bpf_get_current_pid_tgid() >> 32;
 bpf_printk("BPF triggered from PID %d.\n", pid);
 return 0;
}

假设它叫 hello.bpf.c,新建一个 /path/to/repo 的文件夹并且把它放进去,接下来的步骤:

# 下载安装 ecli 二进制
wget https://aka.pw/bpf-ecli -O /usr/local/ecli && chmod +x /usr/local/ecli
# 使用容器进行编译,生成一个 package.json 文件,里面是已经编译好的代码和一些辅助信息
docker run -it -v /path/to/repo:/src yunwei37/ebpm:latest
# 运行 eBPF 程序(root shell)
sudo ecli run package.json

使用 docker 的时候需要把包含 .bpf.c 文件的目录挂载到容器的 /src 目录下,目录中只有一个 .bpf.c 文件;

它会追踪所有进行 write 系统调用的进程的 pid:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
cat-42755   [003] d...1 48755.529860: bpf_trace_printk: BPF triggered from PID 42755.
             cat-42755   [003] d...1 48755.529874: bpf_trace_printk: BPF triggered from PID 42755.

我们编译好的 eBPF 代码同样可以适配多种内核版本,可以直接把 package.json 复制到另外一个机器上,然后不需要重新编译就可以直接运行(CO-RE:Compile Once Run Every Where);也可以通过网络传输和分发 package.json,通常情况下,压缩后的版本只有几 kb 到几十 kb。

添加 map 记录数据

参考:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/bootstrap


struct {
 __uint(type, BPF_MAP_TYPE_HASH);
 __uint(max_entries, 8192);
 __type(key, pid_t);
 __type(value, u64);
} exec_start SEC(".maps");

添加 map 的功能和 libbpf 没有任何区别,只需要在 .bpf.c 中定义即可。

使用 ring buffer 往用户态发送数据

参考:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/bootstrap

只需要定义一个头文件,包含你想要发送给用户态的数据格式,以 .h 作为后缀名:

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2020 Facebook */
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
 int pid;
 int ppid;
 unsigned exit_code;
 unsigned long long duration_ns;
 char comm[TASK_COMM_LEN];
 char filename[MAX_FILENAME_LEN];
 unsigned char exit_event;
};

#endif /* __BOOTSTRAP_H */

在代码中定义环形缓冲区之后,就可以直接使用它:

struct {
 __uint(type, BPF_MAP_TYPE_RINGBUF);
 __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    ......
 e->exit_event = false;
 e->pid = pid;
 e->ppid = BPF_CORE_READ(task, real_parent, tgid);
 bpf_get_current_comm(&e->comm, sizeof(e->comm));
 /* successfully submit it to user-space for post-processing */
 bpf_ringbuf_submit(e, 0);
 return 0;
}

eunomia-bpf 会自动去源代码中找到对应的 ring buffer map,并且把 ring buffer 和类型信息记录在编译好的信息中,并在运行的时候自动完成对于 ring buffer 的加载、导出事件等工作。所有的 eBPF 代码和原生的 libbpf 程序没有任何区别,使用 eunomia-bpf 开发的代码也可以在 libbpf 中无需任何改动即可编译运行。

使用 perf event array 往用户态发送数据

使用 perf event 的原理和使用 ring buffer 非常类似,使用我们的框架时,也只需要在头文件中定义好所需导出的事件,然后定义一下 perf event map:

struct {
 __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
 __uint(key_size, sizeof(u32));
 __uint(value_size, sizeof(u32));
} events SEC(".maps");

可以参考:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/opensnoop 它是直接从 libbpf-tools 中移植的实现;

使用 github-template 实现远程编译

由于 eunomia-bpf 的编译和运行阶段完全分离,可以实现在 github 网页上编辑之后,通过 github actions 来完成编译,之后在本地一行命令即可启动:

  1. 将此 github.com/eunomia-bpf/ebpm-template 用作 github 模板:请参阅 creating-a-repository-from-a-template
  2. 修改 bootstrap.bpf.c, commit 并等待工作流停止
  3. 我们配置了 github pages 来完成编译好的 json 的导出,之后就可以实现 ecli 使用远程 url 一行命令即可运行:
sudo ./ecli run https://eunomia-bpf.github.io/ebpm-template/package.json

通过 API 进行热插拔和分发

由于 eunomia-cc 编译出来的 ebpf 程序代码和附加信息很小(约数十 kb),且不需要同时传递任何的额外依赖,因此我们可以非常方便地通过网络 API 直接进行分发,也可以在很短的时间(大约 100ms)内实现热插拔和热更新。我们提供了一个简单的 client 和 server,请参考;

https://github.com/eunomia-bpf/eunomia-bpf/blob/master/documents/ecli-usage.md

之前也有一篇比赛项目的可行性验证的文章:

https://zhuanlan.zhihu.com/p/555362934

使用 Prometheus 或 OpenTelemetry 进行可观测性数据收集

基于 async Rust 的 Prometheus 或 OpenTelemetry 自定义可观测性数据收集器: eunomia-exporter

可以自行编译或通过 release 下载

example

这是一个 opensnoop 程序,追踪所有的打开文件,源代码来自 bcc/libbpf-tools, 我们修改过后的源代码在这里: examples/bpftools/opensnoop

在编译之后,可以定义一个这样的配置文件:

programs:
  - name: opensnoop
    metrics:
      counters:
        - name: eunomia_file_open_counter
          description: test
          labels:
            - name: pid
            - name: comm
            - name: filename
              from: fname
    compiled_ebpf_filename: examples/bpftools/opensnoop/package.json

然后,您可以在任何地方使用 config.yaml 和预编译的 eBPF 数据 package.json 启动 Prometheus 导出器,您可以看到如下指标:

prometheus

您可以在任何内核版本上部署导出器,而无需依赖 LLVM/Clang。 有关详细信息,请参阅 eunomia-exporter

使用 WASM 模块分发、动态加载 eBPF 程序

eunomia-bpf 库包含一个简单的命令行工具(ecli),包含了一个小型的 WASM 运行时模块和 eBPF 动态装载的功能,可以直接下载下来后进行使用:

# download the release from https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecli
$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
$ sudo ./ecli run https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm
2022-10-11 14:05:50 URL:https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm [70076/70076] -> "/tmp/ebpm/app.wasm" [1]
running and waiting for the ebpf events from perf event...
{"pid":1709490,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1712603,"tpid":1717412,"sig":2,"ret":0,"comm":"kworker/u4:3","sig_name":"SIGINT"}
{"pid":1712603,"tpid":1717411,"sig":2,"ret":0,"comm":"kworker/u4:3","sig_name":"SIGINT"}
{"pid":0,"tpid":847,"sig":14,"ret":0,"comm":"swapper/1","sig_name":"SIGALRM"}
{"pid":1709490,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1709139,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1717420,"tpid":1717419,"sig":17,"ret":0,"comm":"cat","sig_name":"SIGCHLD"}

ecli 会自动从网页上下载并加载 sigsnoop/app.wasm 这个 wasm 模块,它包含了一个 eBPF 程序,用于跟踪内核中进程的信号发送和接收。这里我们可以看到一个简单的 JSON 格式的输出,包含了进程的 PID、信号的类型、发送者和接收者,以及信号名称等信息。它也可以附带一些命令行参数,例如:

$ wget https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm
2022-10-11 14:08:07 (40.5 MB/s) - ‘app.wasm.1’ saved [70076/70076]

$ sudo ./ecli run app.wasm -h
Usage: sigsnoop [-h] [-x] [-k] [-n] [-p PID] [-s SIGNAL]
Trace standard and real-time signals.


    -h, --help  show this help message and exit
    -x, --failed  failed signals only
    -k, --killed  kill only
    -p, --pid=<int>  target pid
    -s, --signal=<int>  target signal

$ sudo ./ecli run app.wasm -p 1641
running and waiting for the ebpf events from perf event...
{"pid":1641,"tpid":14900,"sig":23,"ret":0,"comm":"YDLive","sig_name":"SIGURG"}
{"pid":1641,"tpid":14900,"sig":23,"ret":0,"comm":"YDLive","sig_name":"SIGURG"}

我们可以通过 -p 控制它追踪哪个进程,在内核态 eBPF 程序中进行一些过滤和处理。同样也可以使用 ecli 来动态加载使用其他的工具,例如 opensnoop:

$ sudo ./ecli run https://eunomia-bpf.github.io/eunomia-bpf/opensnoop/app.wasm
2022-10-11 14:11:56 URL:https://eunomia-bpf.github.io/eunomia-bpf/opensnoop/app.wasm [61274/61274] -> "/tmp/ebpm/app.wasm" [1]
running and waiting for the ebpf events from perf event...
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/1718823/cmdline"}
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/1718824/cmdline"}
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/self/stat"}

opensnoop 会追踪进程的 open() 调用,即内核中所有的打开文件操作,这里我们可以看到进程的 PID、UID、返回值、调用标志、进程名和文件名等信息。内核态的 eBPF 程序会被包含在 WASM 模块中进行分发,在加载的时候通过 BTF 信息和 libbpf 进行重定位操作,以适应不同的内核版本。同时,由于用户态的相关处理代码完全由 WASM 编写,内核态由 eBPF 指令编写,因此不受具体指令集(x86、ARM 等)的限制,可以在不同的平台上运行。

使用 WASM 开发和打包 eBPF 程序

同样,以上文所述的 sigsnoop 为例,要跟踪进程的信号发送和接收,我们首先需要在 sigsnoop.bpf.c 中编写内核态的 eBPF 代码:

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include "sigsnoop.h"

const volatile pid_t filtered_pid = 0;
.....

struct {
 __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
 __uint(key_size, sizeof(__u32));
 __uint(value_size, sizeof(__u32));
} events SEC(".maps");

SEC("tracepoint/signal/signal_generate")
int sig_trace(struct trace_event_raw_signal_generate *ctx)
{
 struct event event = {};
 pid_t tpid = ctx->pid;
 int ret = ctx->errno;
 int sig = ctx->sig;
 __u64 pid_tgid;
 __u32 pid;

 ...
 pid_tgid = bpf_get_current_pid_tgid();
 pid = pid_tgid >> 32;
 if (filtered_pid && pid != filtered_pid)
  return 0;

 event.pid = pid;
 event.tpid = tpid;
 event.sig = sig;
 event.ret = ret;
 bpf_get_current_comm(event.comm, sizeof(event.comm));
 bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
 return 0;
}

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

这里我们使用 tracepoint/signal/signal_generate 这个 tracepoint 来在内核中追踪信号的产生事件。内核态代码通过 BPF_MAP_TYPE_PERF_EVENT_ARRAY 往用户态导出信息,为此我们需要在 sigsnoop.h 头文件,中定义一个导出信息的结构体:

#ifndef __SIGSNOOP_H
#define __SIGSNOOP_H

#define TASK_COMM_LEN 16

struct event {
 unsigned int pid;
 unsigned int tpid;
 int sig;
 int ret;
 char comm[TASK_COMM_LEN];
};

#endif /* __SIGSNOOP_H */

可以直接使用 eunomia-bpf 的编译工具链将其编译为 JSON 格式,生成一个 package.json 文件,并且可以直接使用 ecli 加载运行:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest
make
  BPF      .output/client.bpf.o
  GEN-SKEL .output/client.skel.h
  CC       .output/client.o
  CC       .output/cJSON.o
  CC       .output/create_skel_json.o
  BINARY   client
  DUMP_LLVM_MEMORY_LAYOUT
  DUMP_EBPF_PROGRAM
  FIX_TYPE_INFO_IN_EBPF
  GENERATE_PACKAGE_JSON

$ sudo ./ecli run package.json
running and waiting for the ebpf events from perf event...
time pid tpid sig ret comm
14:39:39 1723835 1723834 17 0 dirname
14:39:39 1723836 1723834 17 0 chmod
14:39:39 1723838 1723837 17 0 ps
14:39:39 1723839 1723837 17 0 grep
14:39:39 1723840 1723837 17 0 grep
14:39:39 1723841 1723837 17 0 wc

我们所有的编译工具链都已经打包成了 docker 镜像的形式并发布到了 docker hub 上,可以直接开箱即用。此时动态加载运行的只有内核态的 eBPF 代码和一些辅助信息,帮助 eunomia-bpf 库自动获取内核态往用户态上报的事件。如果我们想要在用户态进行一些参数配置和调整,以及数据处理流程,我们需要在用户态编写代码,将内核态的 eBPF 代码和用户态的代码打包成一个完整的 eBPF 程序。

可以直接一行命令,生成 eBPF 程序的用户态 WebAssembly 开发框架:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest gen-wasm-skel
make
  GENERATE_PACKAGE_JSON
  GEN-WASM-SKEL
$ ls
app.c eunomia-include ewasm-skel.h package.json README.md  sigsnoop.bpf.c  sigsnoop.h

我们提供的是 C 语言版本的 WASM 开发框架,它包含如下这些文件:

  • ewasm-skel.h:用户态 WebAssembly 开发框架的头文件,包含了预编译的 eBPF 程序字节码,和 eBPF 程序框架辅助信息,用来动态加载;
  • eunomia-include:一些 header-only 的库函数和辅助文件,用来辅助开发;
  • app.c:用户态 WebAssembly 程序的主要代码,包含了 eBPF 程序的主要逻辑,以及 eBPF 程序的数据处理流程。

以 sigsnoop 为例,用户态包含一些命令行解析、配置 eBPF 程序和数据处理的代码,会将根据 signal number 将信号事件的英文名称添加到事件中:

....
int main(int argc, const char** argv)
{
  struct argparse_option options[] = {
        OPT_HELP(),
        OPT_BOOLEAN('x', "failed", &failed_only, "failed signals only", NULL, 0, 0),
        OPT_BOOLEAN('k', "killed", &kill_only, "kill only", NULL, 0, 0),
        OPT_INTEGER('p', "pid", &target_pid, "target pid", NULL, 0, 0),
  OPT_INTEGER('s', "signal", &target_signal, "target signal", NULL, 0, 0),
        OPT_END(),
    };

  struct argparse argparse;
  argparse_init(&argparse, options, usages, 0);
  argparse_describe(&argparse, "Trace standard and real-time signals.\n", "");
  argc = argparse_parse(&argparse, argc, argv);

  cJSON *program = cJSON_Parse(program_data);
  program = set_bpf_program_global_var(program, "filtered_pid", cJSON_CreateNumber(target_pid));
  program = set_bpf_program_global_var(program, "target_signal", cJSON_CreateNumber(target_signal));
  program = set_bpf_program_global_var(program, "failed_only", cJSON_CreateBool(failed_only));
  return start_bpf_program(cJSON_PrintUnformatted(program));
}

int process_event(int ctx, char *e, int str_len)
{
 cJSON *json = cJSON_Parse(e);
 int sig = cJSON_GetObjectItem(json, "sig")->valueint;
 const char *name = sig_name[sig];
 cJSON_AddItemToObject(json, "sig_name", cJSON_CreateString(name));
 char *out = cJSON_PrintUnformatted(json);
 printf("%s\n", out);
 return 0;
}

最后使用容器镜像即可一行命令完成 WebAssembly/eBPF 程序的编译和打包,使用 ecli 即可一键运行:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest build-wasm
make
  GENERATE_PACKAGE_JSON
  BUILD-WASM
build app.wasm success
$ sudo ./ecli run app.wasm -h
Usage: sigsnoop [-h] [-x] [-k] [-n] [-p PID] [-s SIGNAL]

由于我们基于一次编译、到处运行的 libbpf 框架完成加载和启动 eBPF 程序的操作,因此编译和运行两个步骤是完全分离的,可以通过网络或任意方式直接进行 eBPF 程序的分发和部署,不依赖于特定内核版本。借助 WebAssembly 的轻量级特性,eBPF 程序的启动速度也比通常的使用镜像形式分发的 libbpf 程序快上不少,通常只需不到 100 ms 的时间即可完成,比起使用 BCC 部署启动时,使用 LLVM、Clang 编译运行消耗的时间和大量资源,更是有了质的飞跃。

上面提及的示例程序的完整代码,可以参考这里[6]。

演示视频

我们也有一个在 B 站上的演示视频,演示了如何从 bcc/libbpf-tools 中移植一个 eBPF 工具程序到 eunomia-bpf 中,并且使用 WASM 或 JSON 文件来分发、加载 eBPF 程序:https://www.bilibili.com/video/BV1JN4y1A76k

ecli 是基于我们底层的 eunomia-bpf 库和运行时实现的一个简单的命令行工具。我们的项目架构如下图所示:

arch

ecli 工具基于 ewasm 库实现,ewasm 库包含一个 WAMR(wasm-micro-runtime) 运行时,以及基于 libbpf 库构建的 eBPF 动态装载模块。大致来说,我们在 WASM 运行时和用户态的 libbpf 中间多加了一层抽象层(eunomia-bpf 库),使得一次编译、到处运行的 eBPF 代码可以从 JSON 对象中动态加载。JSON 对象会在编译时被包含在 WASM 模块中,因此在运行时,我们可以通过解析 JSON 对象来获取 eBPF 程序的信息,然后动态加载 eBPF 程序。

使用 WASM 或 JSON 编译分发 eBPF 程序的流程图大致如下:

flow

大致来说,整个 eBPF 程序的编写和加载分为三个部分:

  1. 用 eunomia-cc 工具链将内核的 eBPF 代码骨架和字节码编译为 JSON 格式
  2. 在用户态开发的高级语言(例如 C 语言)中嵌入 JSON 数据,并提供一些 API 用于操作 JSON 形态的 eBPF 程序骨架
  3. 将用户态程序和 JSON 数据一起编译为 WASM 字节码并打包为 WASM 模块,然后在目标机器上加载并运行 WASM 程序
  4. 从 WASM 模块中加载内嵌的 JSON 数据,用 eunomia-bpf 库动态装载和配置 eBPF 程序骨架。

我们需要完成的仅仅是少量的 native API 和 WASM 运行时的绑定,并且在 WASM 代码中处理 JSON 数据。你可以在一个单一的 WASM 模块中拥有多个 eBPF 程序。如果不使用我们提供的 WASM 运行时,或者想要使用其他语言进行用户态的 eBPF 辅助代码的开发,在我们提供的 eunomia-bpf 库基础上完成一些 WebaAssembly 的绑定即可。

另外,对于 eunomia-bpf 库而言,不需要 WASM 模块和运行时同样可以启动和动态加载 eBPF 程序,不过此时动态加载运行的就只是内核态的 eBPF 程序字节码。你可以手动或使用任意语言修改 JSON 对象来控制 eBPF 程序的加载和参数,并且通过 eunomia-bpf 自动获取内核态上报的返回数据。对于初学者而言,这可能比使用 WebAssembly 更加简单方便:只需要编写内核态的 eBPF 程序,然后使用 eunomia-cc 工具链将其编译为 JSON 格式,最后使用 eunomia-bpf 库加载和运行即可。完全不用考虑任何用户态的辅助程序,包括 WASM 在内。具体可以参考我们的使用手册[7]或示例代码[8]。

原理

ecli 是基于我们底层的 eunomia-bpf 库和运行时实现的一个简单的命令行工具。我们的项目架构如下图所示:

arch

ecli 工具基于 ewasm 库实现,ewasm 库包含一个 WAMR(wasm-micro-runtime) 运行时,以及基于 libbpf 库构建的 eBPF 动态装载模块。大致来说,我们在 WASM 运行时和用户态的 libbpf 中间多加了一层抽象层(eunomia-bpf 库),使得一次编译、到处运行的 eBPF 代码可以从 JSON 对象中动态加载。JSON 对象会在编译时被包含在 WASM 模块中,因此在运行时,我们可以通过解析 JSON 对象来获取 eBPF 程序的信息,然后动态加载 eBPF 程序。

使用 WASM 或 JSON 编译分发 eBPF 程序的流程图大致如下:

graph TD
  b3-->package
  b4-->a3
  package-->a1
  package(可通过网络或其他任意方式进行分发: CO-RE)

  subgraph 运行时加载器库
  a3(运行 WASM 模块配置 eBPF 程序或和 eBPF 程序交互)
  a1(根据 JSON 配置信息动态装载 eBPF 程序)
  a2(根据类型信息和内存布局信息对内核态导出事件进行动态处理)
  a1-->a2
  a3-->a1
  a2-->a3
  end

  subgraph eBPF编译工具链
  b1(使用 Clang 编译 eBPF 程序获得包含重定位信息的 bpf.o)
  b2(添加从 eBPF 源代码获取的内核态导出数据的内存布局, 类型信息等)
  b3(打包生成 JSON 数据)
  b4(打包成 WASM 模块进行分发)
  b5(可选的用户态数据处理程序编译为 WASM)
  b2-->b3
  b3-->b5
  b5-->b4
  b1-->b2
  end

大致来说,整个 eBPF 程序的编写和加载分为三个部分:

  1. 用 eunomia-cc 工具链将内核的 eBPF 代码骨架和字节码编译为 JSON 格式
  2. 在用户态开发的高级语言(例如 C 语言)中嵌入 JSON 数据,并提供一些 API 用于操作 JSON 形态的 eBPF 程序骨架
  3. 将用户态程序和 JSON 数据一起编译为 WASM 字节码并打包为 WASM 模块,然后在目标机器上加载并运行 WASM 程序
  4. 从 WASM 模块中加载内嵌的 JSON 数据,用 eunomia-bpf 库动态装载和配置 eBPF 程序骨架。

我们需要完成的仅仅是少量的 native API 和 WASM 运行时的绑定,并且在 WASM 代码中处理 JSON 数据。你可以在一个单一的 WASM 模块中拥有多个 eBPF 程序。如果不使用我们提供的 WASM 运行时,或者想要使用其他语言进行用户态的 eBPF 辅助代码的开发,在我们提供的 eunomia-bpf 库基础上完成一些 WebaAssembly 的绑定即可。

另外,对于 eunomia-bpf 库而言,不需要 WASM 模块和运行时同样可以启动和动态加载 eBPF 程序,不过此时动态加载运行的就只是内核态的 eBPF 程序字节码。你可以手动或使用任意语言修改 JSON 对象来控制 eBPF 程序的加载和参数,并且通过 eunomia-bpf 自动获取内核态上报的返回数据。对于初学者而言,这可能比使用 WebAssembly 更加简单方便:只需要编写内核态的 eBPF 程序,然后使用 eunomia-cc 工具链将其编译为 JSON 格式,最后使用 eunomia-bpf 库加载和运行即可。完全不用考虑任何用户态的辅助程序,包括 WASM 在内。具体可以参考我们的使用手册[7]或示例代码[8]。

为我们的项目贡献代码

我们的项目还在早期阶段,因此非常希望有您的帮助:

eunomia-bpf 也已经加入了龙蜥社区:

您可以帮助我们添加测试或者示例,可以参考:

由于现在 API 还不稳定,如果您在试用中遇到任何问题或者任何流程/文档不完善的地方,请在 gitee 或 github issue 留言, 我们会尽快修复;也非常欢迎进一步的 PR 提交和贡献!也非常希望您能提出一些宝贵的意见或者建议!

架构设计

Eunomia 包含如下几个项目:

  • eunomia-bpf:一个基于 libbpf 的 CO-RE eBPF 运行时库,使用 C/C++ 语言。提供 Rust 等语言的 sdk;提供 ecli 作为命令行工具;
  • eunomia-cc:一个编译工具链;
  • ewasm: 一个基于 eunomia-bpf 的 wasm 运行时库,提供基于 wasm 的 eBPF 程序开发能力;
  • eunomia-exporter:使用 Prometheus 或 OpenTelemetry 进行可观测性数据收集,使用 Rust 编写;
  • ebpm-template:使用 Github Action 进行远程编译,本地一键运行;

目录:

架构图

arch

项目概览

eunomia-bpf 包含如下几个项目:

  • eunomia-bpf:一个基于 libbpf 的 CO-RE eBPF 运行时库,使用 C/C++ 语言。提供 Rust 等语言的 sdk;提供 ecli 作为命令行工具;
  • eunomia-cc:一个编译工具链;
  • eunomia-exporter:使用 Prometheus 或 OpenTelemetry 进行可观测性数据收集,使用 Rust 编写;
  • ebpm-template:使用 Github Action 进行远程编译,本地一键运行;

一个 eunomia-bpf 库

libbpf 主要功能的封装,一些用于用户开发的辅助功能。

  • 提供将 ebpf 代码加载到内核并运行它的能力。
  • 使用一些额外的数据来帮助加载和配置 eBPF 字节码。
  • 多语言绑定:参见 eunomia-sdks。 我们现在有 Rust 的 API,将来会添加更多;

安装运行

大多数时候安装时只需要下载对应的二进制即可:

# download the release from https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecli
wget https://aka.pw/bpf-ecli -O ecli && chmod +x ecli

有关详细信息,请参见 eunomia-bpf 文件夹。 借助该库,我们提供了一个简单的 cli,在支持 eBPF 的内核版本上,您可以简单地使用 url 或路径运行预编译 eBPF 数据:

sudo ./ecli run https://eunomia-bpf.github.io/ebpm-template/package.json # simply run a pre-compiled ebpf code from a url

可以使用容器进行编译, 仅需要专注于编写内核态代码:

docker run -it -v ./examples/bpftools/bootstrap:/src yunwei37/ebpm:latest
sudo ./ecli run examples/bpftools/bootstrap/package.json              # run the compiled ebpf code

更多的例子请参考 examples/bpftools 文件夹.

用于生成预编译 eBPF 数据的编译工具链

有关详细信息,请参阅编译工具链 eunomia-cc

您也可以简单地使用 ebpm-template repo 作为 github 中的模板开始编写代码,只需推送后,Github Actions 即可以帮助您编译 CO-RE ebpf 代码!

一个可观测性工具

基于 async Rust 的 Prometheus 或 OpenTelemetry 自定义可观测性数据收集器: eunomia-exporter

可以自行编译或通过 release 下载

example

这是一个 opensnoop 程序,追踪所有的打开文件,源代码来自 bcc/libbpf-tools, 我们修改过后的源代码在这里: examples/bpftools/opensnoop

在编译之后,可以定义一个这样的配置文件:

programs:
  - name: opensnoop
    metrics:
      counters:
        - name: eunomia_file_open_counter
          description: test
          labels:
            - name: pid
            - name: comm
            - name: filename
              from: fname
    compiled_ebpf_filename: examples/bpftools/opensnoop/package.json

然后,您可以在任何地方使用 config.yaml 和预编译的 eBPF 数据 package.json 启动 Prometheus 导出器,您可以看到如下指标:

prometheus

您可以在任何内核版本上部署导出器,而无需依赖 LLVM/Clang。 有关详细信息,请参阅 eunomia-exporter

工作原理

ecli 是基于我们底层的 eunomia-bpf 库和运行时实现的一个简单的命令行工具。我们的项目架构如下图所示:

arch

ecli 工具基于 ewasm 库实现,ewasm 库包含一个 WAMR(wasm-micro-runtime) 运行时,以及基于 libbpf 库构建的 eBPF 动态装载模块。大致来说,我们在 WASM 运行时和用户态的 libbpf 中间多加了一层抽象层(eunomia-bpf 库),使得一次编译、到处运行的 eBPF 代码可以从 JSON 对象中动态加载。JSON 对象会在编译时被包含在 WASM 模块中,因此在运行时,我们可以通过解析 JSON 对象来获取 eBPF 程序的信息,然后动态加载 eBPF 程序。

使用 WASM 或 JSON 编译分发 eBPF 程序的流程图大致如下:

flow

大致来说,整个 eBPF 程序的编写和加载分为三个部分:

  1. 用 eunomia-cc 工具链将内核的 eBPF 代码骨架和字节码编译为 JSON 格式
  2. 在用户态开发的高级语言(例如 C 语言)中嵌入 JSON 数据,并提供一些 API 用于操作 JSON 形态的 eBPF 程序骨架
  3. 将用户态程序和 JSON 数据一起编译为 WASM 字节码并打包为 WASM 模块,然后在目标机器上加载并运行 WASM 程序
  4. 从 WASM 模块中加载内嵌的 JSON 数据,用 eunomia-bpf 库动态装载和配置 eBPF 程序骨架。

我们需要完成的仅仅是少量的 native API 和 WASM 运行时的绑定,并且在 WASM 代码中处理 JSON 数据。你可以在一个单一的 WASM 模块中拥有多个 eBPF 程序。如果不使用我们提供的 WASM 运行时,或者想要使用其他语言进行用户态的 eBPF 辅助代码的开发,在我们提供的 eunomia-bpf 库基础上完成一些 WebaAssembly 的绑定即可。

另外,对于 eunomia-bpf 库而言,不需要 WASM 模块和运行时同样可以启动和动态加载 eBPF 程序,不过此时动态加载运行的就只是内核态的 eBPF 程序字节码。你可以手动或使用任意语言修改 JSON 对象来控制 eBPF 程序的加载和参数,并且通过 eunomia-bpf 自动获取内核态上报的返回数据。对于初学者而言,这可能比使用 WebAssembly 更加简单方便:只需要编写内核态的 eBPF 程序,然后使用 eunomia-cc 工具链将其编译为 JSON 格式,最后使用 eunomia-bpf 库加载和运行即可。完全不用考虑任何用户态的辅助程序,包括 WASM 在内。具体可以参考我们的使用手册[7]或示例代码[8]。

命令行工具

help info

SYNOPSIS
        ecli/build/bin/Release/ecli [--log-level <log level>] client list [--endpoint <server
            endpoint>]

        ecli/build/bin/Release/ecli [--log-level <log level>] client start <url> [<extra
            args>]... [--endpoint <server endpoint>]

        ecli/build/bin/Release/ecli [--log-level <log level>] client stop <stop id> [--endpoint
            <server endpoint>]

        ecli/build/bin/Release/ecli [--log-level <log level>] run <url> [<extra args>]...
        ecli/build/bin/Release/ecli [--log-level <log level>] server [--config <config file>]
        ecli/build/bin/Release/ecli [--log-level <log level>] help

OPTIONS
        --log-level <log level>
                    The log level for the eunomia cli, can be debug, info, warn, error

        use client to control the ebpf programs in remote server
            list    list the ebpf programs running on endpoint
            start   start an ebpf programs on endpoint
            <url>   The url to get the ebpf program, can be file path or url
            <extra args>...
                    Some extra args provided to the ebpf program

            stop    stop an ebpf programs on endpoint
            <stop id>
                    The id of the ebpf program to stop in sercer

            --endpoint <server endpoint>
                    The endpoint of server to connect to

        run a ebpf program
            <url>   The url to get the ebpf program, can be file path or url
            <extra args>...
                    Some extra args provided to the ebpf program

        start a server to control the ebpf programs
            --config <config file>
                    The json file stores the config data

ecli server

start a server:

$ sudo ecli/build/bin/Release/ecli server
[2022-08-22 22:43:36.201] [info] start server mode...
[2022-08-22 22:43:36.201] [info] start eunomia...
[2022-08-22 22:43:36.201] [info] eunomia server start at port 8527

use client to communicate with the server:

$ sudo ./ecli client list
200 :["status","ok","list",[]]

$ sudo ./ecli client start https://eunomia-bpf.github.io/ebpm-template/package.json
2022-08-22 22:44:37 URL:https://eunomia-bpf.github.io/ebpm-template/package.json [42181] -> "/tmp/ebpm/package.json" [1]
200 :["status","ok","id",1]

$ sudo ./ecli client list
200 :["status","ok","list",[[1,"execsnoop"]]]

$ sudo ./ecli client stop 1
200 :["status","ok"]

$ sudo ./ecli client list
200 :["status","ok","list",[]]

ecli run

run an pre-compiled ebpf program:

$ sudo ./ecli run https://gitee.com/yunwei37/eunomia-bpf/raw/master/examples/bpftools/package.json
$ sudo ./ecli run https://github.com/eunomia-bpf/eunomia-bpf/raw/master/examples/bpftools/package.json
$ sudo ./ecli run examples/bpftools/package.json

eunomia-exporter

An prometheus and OpenTelemetry exporter for custom eBPF metrics, written in async rust: eunomia-exporter

This is a single binary exporter, you don't need to install BCC/LLVM when you use it. The only thing you will need to run the exporter on another machine is the config file and pre-compiled eBPF code.

Supported scenarios

Currently the only supported way of getting data out of the kernel is via maps (we call them tables in configuration).

example

This is an adapted version of opensnoop from bcc/libbpf-tools, you can check our source code here: examples/bpftools/opensnoop

You can just download the pre-compiled opensnoop package.json.

Or you can compile the opensnoop like this:

$ cd examples/bpftools/opensnoop
$ docker run -it -v /userpath/eunomia-bpf/examples/bpftools/opensnoop:/src yunwei37/ebpm:latest

userpath needs to be replaced with your own repo path.

After compile the eBPF code, you can define a config file like this:

programs:
  - name: opensnoop
    metrics:
      counters:
        - name: eunomia_file_open_counter
          description: test
          labels:
            - name: pid
            - name: comm
            - name: filename
              from: fname
    compiled_ebpf_filename: package.json

use the path to package.json as compiled_ebpf_filename in the config file. You can find the example at config.yaml.

Then, you can start the exporter:

$ ls
config.yaml  eunomia-exporter package.json
$ sudo ./eunomia-exporter

Running ebpf program opensnoop takes 46 ms
Listening on http://127.0.0.1:8526
running and waiting for the ebpf events from perf event...
Receiving request at path /metrics

Different from the bcc ebpf_exporter, the only thing you need to run on the deployment machine is the config file and package.json. There is no need to install LLVM/CLang for BCC.

The result is:

prometheus

manage eBPF tracing program via API

start an eBPF exporter via web API:

$ curl -X POST http://127.0.0.1:8526/start -H "Content-Type: application/json" -d @examples/opensnoop/curl_post_example.json

{"id":1}

see curl_post_example.json for the example of the request body.

list all running eBPF programs:

$ curl http://127.0.0.1:8526/list

[{"id":0,"name":"bootstrap"},{"id":1,"name":"opensnoop"}]

stop an eBPF program:

$ curl -X POST http://127.0.0.1:8526/stop -H "Content-Type: application/json" -d '{"id": 1}'

documents:

$ cargo build --release
$ target/release/eunomia-exporter -h
eunomia-exporter 0.1.0

USAGE:
    eunomia-exporter [OPTIONS]

OPTIONS:
    -c, --config <CONFIG>    Sets a custom config file [default: config.yaml]
    -h, --help               Print help information
    -V, --version            Print version information

benchmark

Take opensnoop from bcc/libbpf-tools as an example. starting with BCC, you will need about 0.8s to start the exporter and attach to the probe. With out implement, you only need about 50-70ms which is significantly faster.

$ ps -aux | grep eunomia
root      171562  0.0  0.0  15176  4576 pts/6    S+   01:08   0:00 sudo ./eunomia-exporter
root      171605  0.1  0.0 350540  7740 pts/6    Sl+  01:08   0:00 ./eunomia-exporter

The memory usage and CPU usage is also low.

设计草稿

对比基于 bcc 的 cloudflare/ebpf_exporter,并给出一些简单的设计草稿

基于 bcc 的 cloudflare/ebpf_exporter

eBPF Exporter 是一个将自定义BPF跟踪数据导出到prometheus的工具,它实现了prometheus获取数据的API,prometheus可以通过这些API主动拉取到自定义的BPF跟踪数据。

具体来说,我们只需要编写一个yaml的配置文件,在配置文件中嵌入BPF代码,运行ebpf_exporter就可以实现导出BPF跟踪数据,而这些数据是可以被prometheus主动拉取到的,进而实现BPF跟踪数据的存储、处理和可视化展示。本文档可用于lmp项目数据采集和实现分布式做参考。

  • 基于 bcc,运行 exporter 的时候需要安装配置复杂的 bcc 工具链;
  • 需要手动使用配置文件管理 eBPF 程序;
  • 和 Prometheus 绑定;

eunomia-exporter

  • 使用 yaml 配置文件来完成自定义 eBPF 指标导出,只需要编写好内核态的 eBPF 代码,编译之后只需要编写一个yaml的配置文件,并加入编译好的 eBPF 字节码信息,运行ebpf_exporter就可以实现导出 BPF 跟踪数据;类似 ebpf_exporter;

  • 编译阶段和运行阶段完全分离,可以在本地编译后在服务端上运行,也可以在远程编译后一键在本地运行,这样运行的时候就不需要安装 BCC 环境;

  • 可以使用 web API 直接进行 eBPF 程序的热加载、停止和管理,省去了再开发一套 API 进行管理的工作量,可以和后端服务直接对接;也更方便 LMP 插件化的配置手段;

  • cloudflare/ebpf_exporter 是基于 bcc 的,需要使用 BCC 的内核态代码,而我们目前还有很多工具是基于 libbpf 的;eunomia-exporter 使用的也是 libbpf 的内核态代码,基于 libbpf 的 eBPF 大部分工具使用 eunomia-exporter 几乎不需要修改内核态代码,即可以完成自定义指标导出的过程,但如果使用 cloudflare/ebpf_exporter 的话,还需要将 libbpf 的内核态 BPF 代码移植回 BCC 写的;

  • 使用 rust 的异步运行时编写,二进制体积小、无复杂依赖、性能高(打包以后的镜像可以比 ebpf_exporter 更小);更轻量级

  • 使用了最新的 Opentelemetry SDK 作为 exporter,有非常好的可扩展性,和更完善的可观测性语义支持(Log、metrics、Trace),不仅支持导出到 prometheus (主要适合 metric 类型的信息),也可以导出到其他类型的可观测性组件,例如 Jaeger;

  • 只需要写内核态的代码和一些配置信息,和具体用户态的语言生态无关,不需要考虑用户态的接口是 python 还是 go,还是 c;只要底层是 libbpf 的就能用;(go-libbpf、 rust-libbp、C-libbpf),也可以帮助统一输出格式,不需要为每个程序再写一份用户态的加载代码和入口代码;

也许可以使用 eunomia-exporter 对于基于 libbpf 的追踪器进行统一的指标导出,使用 cloudflare/ebpf_exporter 对于基于 BCC 的追踪器进行统一的指标导出?通过统一的 API 和后端对接和管理;

说不定 eunomia-exporter 也可以起到替代许老师之前说的,使用 BCC 的一个统一的命令行接口的效果;

eunomia-cc:编译工具链

An CO-RE compile set to help you focus on writing a single eBPF program in the kernel. Nothing more TODO!

Github Action 模板

ebpm-template:使用 Github Action 进行远程编译,本地一键运行;

请参考:https://github.com/eunomia-bpf/ebpm-template

A template for eunomia-bpf programs

This is a template for eunomia-bpf eBPF programs. You can use t as a template, compile it online with Github Actions or offline.

Compile and run the eBPF code as simple as possible!

Download the pre-compiled ecli binary from here: eunomia-bpf/eunomia-bpf

To install, just download and use the ecli binary from here: eunomia-bpf/eunomia-bpf:

wget https://aka.pw/bpf-ecli -O ecli && chmod +x ecli

use this repo as a github action to compile online

  1. use this repo as a github template: see creating-a-repository-from-a-template
  2. modify the bootstrap.bpf.c, commit it and wait for the workflow to stop
  3. Run the ecli with remote url:
$ sudo ./ecli run https://eunomia-bpf.github.io/ebpm-template/package.json

quick start

just write some code in the bootstrap.bpf.c, after that, simply run this:

$ docker run -it -v /path/to/repo:/src yunwei37/ebpm:latest # use absolute path

you will get a package.json in your root dir. Just run:

$ sudo ./ecli run package.json

The ebpf compiled code can run on different kernel versions(CO-RE). You can just copied the json to another machine. see: github.com/eunomia-bpf/eunomia-bpf for the runtime, and eunomia-bpf/eunomia-cc for our compiler tool chains.

The code here

This is an example of ebpf code, we copied the bootstrap.bpf.c from libbpf-bootstrap, without any modification. You can read their README for details: https://github.com/libbpf/libbpf-bootstrap

more examples

for more examples, please see: eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools

在线体验网站

可使用 bolipi 提供的在线体验服务,在线编译,在线运行、在线获取可视化结果:https://bolipi.com/ebpf/home/online

imga

通过在线编译运行快速体验 eBPF 和可视化

本在线编译平台由 eunomia-bpf 工具链提供支持,详细文档请参考 eunomia-bpf.github.io/

在线编译

在代码编辑器中编写 eBPF 的内核态程序,应当遵循 libbpf-tools 的内核态代码编写约定,即:

  • 代码编辑器 (*.bpf.c) 包含 BPF C 代码,它被编译成 package.json
  • 头文件编辑器 (*.h) 可以选择包含通过 perf event 或环形缓冲区导出到用户空间的类型

我们目前只支持使用基于 libbpf 的内核态代码,BCC 代码支持由于存在一些语法上的差异,还在开发中。

编写完成代码后,点击 编译 按钮即可编译成 eBPF 的内核态程序,在 编译输出 中查看编译输出:

imgb

更多信息请参考:eunomia-bpf.github.io/mannual.html

更多例子请参考:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools

在线运行

点击右侧的绿色运行按钮运行:

imgc

也可以通过 下载编译后的文件 查看编译好的程序,并在本地使用 ecli 直接运行:

$ # 下载安装 ecli 二进制
$ wget https://aka.pw/bpf-ecli -O ./ecli && chmod +x ./ecli
$ # 运行 eBPF 程序(root shell)
$ sudo ./ecli run package.json

使用 Prometheus 在线获取可视化结果

点击 运行可视化组件 按钮,在弹出的窗口中配置 prometheus metrics 信息:

imgd

点击 确定 即可跳转到 Prometheus 界面,可通过选择 graph 查看可视化结果:

imgd

关于 eunomia-bpf

eunomia-bpf 是一套编译工具链和运行时,以及一些附加项目,我们希望做到让 eBPF 程序:

  • 让 eBPF 程序的编译和运行过程大大简化,抛去繁琐的用户态模板编写、繁琐的 BCC 安装流程,只需要编写内核态 eBPF 程序,编译后即可在不同机器上任意内核版本下运行,并且轻松获取可视化结果。
  • 真正像 JavaScript 或者 WASM 那样易于分发和运行,或者说内核态或可观测性层面的 FaaS:eBPF 即服务,通过 API 请求快速分发和运行,无需管理基础设施和用户态加载程序;

Usage

Usage

The only file you will need to write is:

your_program.bpf.c
your_program.h  # optional, if you want to use ring buffer to export events

after that, simply run this:

$ docker run -it -v /path/to/repo/:/src yunwei37/ebpm:latest # use absolute path

you will get a package.json in your root dir. Just run:

$ sudo ./ecli run package.json

to start it you can download ecli tool from eunomia-bpf/releases, we have pre-build binaries for linux x86. Small and No dependencies, besides glibc and glibcxx. Or just run this:

$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ecli

The eBPF compiled code can run on different kernel versions(CO-RE). see: github.com/eunomia-bpf/eunomia-bpf for details.

container image

simply run:

$ docker run -it -v /path/to/repo:/src yunwei37/ebpm

Or you can do that without a container, which is listed below:

Github actions

Use this as a github action, to compile online: see eunomia-bpf/ebpm-template). Only three steps

  1. use this repo as a github template: see creating-a-repository-from-a-template
  2. modify the bootstrap.bpf.c, commit it and wait for the workflow to stop
  3. Run the ecli with remote url:
$ sudo ./ecli run https://eunomia-bpf.github.io/ebpm-template/package.json

Notifications

  1. We use the same c ebpf code as libbpf, so most libbpf ebpf c code can run without any modification.

  2. Supported ebpf program types: kprobe, tracepoint, fentry, we will add more types in the future.

  3. If you want to use ring buffer to export events, you need to add your_program.h to your repo, and define the export data type in it, the export data type should be a C struct, for example:

    struct process_event {
        int pid;
        int ppid;
        unsigned exit_code;
        unsigned long long duration_ns;
        char comm[TASK_COMM_LEN];
        char filename[MAX_FILENAME_LEN];
        int exit_event;
    };
    

    The name and field types are not limited, but we will prefer use standard C types. If multiple struct exists in the header, we will use the first one. The feature is only enabled if we found a BPF_MAP_TYPE_RINGBUF map exists in the ebpf program.

#!https://zhuanlan.zhihu.com/p/573941739

当 WASM 遇见 eBPF :使用 WebAssembly 编写、分发、加载运行 eBPF 程序

当今云原生世界中两个最热门的轻量级代码执行沙箱/虚拟机是 eBPF 和 WebAssembly。它们都运行从 C、C++ 和 Rust 等语言编译的高性能字节码程序,并且都是跨平台、可移植的。二者最大的区别在于: eBPF 在 Linux 内核中运行,而 WebAssembly 在用户空间中运行。我们希望能做一些将二者相互融合的尝试:使用 WASM 来编写通用的 eBPF 程序,然后可以将其分发到任意不同版本、不同架构的 Linux 内核中,无需重新编译即可运行。

WebAssembly vs. eBPF

WebAssembly(缩写 Wasm)是基于堆栈虚拟机的二进制指令格式。Wasm 是为了一个可移植的目标而设计的,可作为 C/C+/RUST 等高级语言的编译目标,使客户端和服务器应用程序能够在 Web 上部署。WASM 的运行时有多种实现,包括浏览器和独立的系统,它可以用于视频和音频编解码器、图形和 3D、多媒体和游戏、密码计算或便携式语言实现等应用。

尽管 WASM 是为了提高网页中性能敏感模块表现而提出的字节码标准, 但是 WASM 却不仅能用在浏览器(broswer)中, 也可以用在其他环境中。WASM 已经发展成为一个轻量级、高性能、跨平台和多语种的软件沙盒环境,被运用于云原生软件组件。与 Linux 容器相比,WebAssembly 的启动速度可以提高 100 倍,内存和磁盘占用空间要小得多,并且具有更好定义的安全沙箱。然而,权衡是 WebAssembly 需要自己的语言 SDK 和编译器工具链,使其成为比 Linux 容器更受限制的开发环境。WebAssembly 越来越多地用于难以部署 Linux 容器或应用程序性能至关重要的边缘计算场景。

WASM 的编译和部署流程如下:

wasm-compile-deploy

通常可以将 C/C+/RUST 等高级语言编译为 WASM 字节码,在 WASM 虚拟机中进行加载运行。WASM 虚拟机会通过解释执行或 JIT 的方式,将 WASM 字节码翻译为对应平台( x86/arm 等)的机器码运行。

eBPF 源于 BPF,本质上是处于内核中的一个高效与灵活的虚拟机组件,以一种安全的方式在许多内核 hook 点执行字节码。BPF 最初的目的是用于高效网络报文过滤,经过重新设计,eBPF 不再局限于网络协议栈,已经成为内核顶级的子系统,演进为一个通用执行引擎。开发者可基于 eBPF 开发性能分析工具、软件定义网络、安全等诸多场景。eBPF 有一些编程限制,需要经过验证器确保其在内核应用场景中是安全的(例如,没有无限循环、内存越界等),但这也意味着 eBPF 的编程模型不是图灵完备的。相比之下,WebAssembly 是一种图灵完备的语言,具有能够打破沙盒和访问原生 OS 库的扩展 WASI (WebAssembly System Interface, WASM 系统接口) ,同时 WASM 运行时可以安全地隔离并以接近原生的性能执行用户空间代码。二者的领域主体上有不少差异,但也有不少相互重叠的地方。

有一些在 Linux 内核中运行 WebAssembly 的尝试,然而基本上不太成功。 eBPF 是这个应用场景下更好的选择。但是 WebAssembly 程序可以处理许多类内核的任务,可以被 AOT 编译成原生应用程序。来自 CNCF 的 WasmEdge Runtime 是一个很好的基于 LLVM 的云原生 WebAssembly 编译器。原生应用程序将所有沙箱检查合并到原生库中,这允许 WebAssembly 程序表现得像一个独立的 unikernel “库操作系统”。此外,这种 AOT 编译的沙盒 WebAssembly 应用程序可以在微内核操作系统(如 seL4)上运行,并且可以接管许多“内核级”任务[1]。

虽然 WebAssembly 可以下降到内核级别,但 eBPF 也可以上升到应用程序级别。在 sidecar 代理中,Envoy Proxy 开创了使用 Wasm 作为扩展机制对数据平面进行编程的方法。开发人员可以用 C、C++、Rust、AssemblyScript、Swift 和 TinyGo 等语言编写特定应用的代理逻辑,并将该模块编译到 Wasm 中。通过 proxy-Wasm 标准,代理可以在 Wasmtime 和 WasmEdge 等高性能运行机制中执行那些 Wasm 插件[2]。

尽管目前有不少应用程序同时使用了二者,但大多数时候这两个虚拟机是相互独立并且没有交集的:例如在可观测性应用中,通过 eBPF 探针获取数据,获取数据之后在用户态引入 WASM 插件模块,进行可配置的数据处理。WASM 模块和 eBPF 程序的分发、运行、加载、控制相互独立,仅仅存在数据流的关联。

我们的一次尝试

一般来说,一个完整的 eBPF 应用程序分为用户空间程序和内核程序两部分:

  • 用户空间程序负责加载 BPF 字节码至内核,或负责读取内核回传的统计信息或者事件详情,进行相关的数据处理和控制;
  • 内核中的 BPF 字节码负责在内核中执行特定事件,如需要也会将执行的结果通过 maps 或者 perf-event 事件发送至用户空间

用户态程序可以在加载 eBPF 程序前控制一些 eBPF 程序的参数和变量,以及挂载点;也可以通过 map 等等方式进行用户态和内核态之间的双向通信。通常来说用户态的 eBPF 程序可以基于 libbpf 库进行开发,来控制内核态 eBPF 程序的装载和运行。那么,如果将用户态的所有控制和数据处理逻辑全部移到 WASM 虚拟机中,通过 WASM module 打包和分发 eBPF 字节码,同时在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,也许我们就可以将二者的优势结合起来,让任意 eBPF 程序能有如下特性:

  • 可移植:让 eBPF 工具和应用完全平台无关、可移植,不需要进行重新编译即可以跨平台分发;
  • 隔离性:借助 WASM 的可靠性和隔离性,让 eBPF 程序的加载和执行、以及用户态的数据处理流程更加安全可靠;事实上一个 eBPF 应用的用户态控制代码通常远远多于内核态;
  • 包管理:借助 WASM 的生态和工具链,完成 eBPF 程序或工具的分发、管理、加载等工作,目前 eBPF 程序或工具生态可能缺乏一个通用的包管理或插件管理系统;
  • 跨语言:目前 eBPF 程序由多种用户态语言开发(如 Go\Rust\C\C++\Python 等),超过 30 种编程语言可以被编译成 WebAssembly 模块,允许各种背景的开发人员(C、Go、Rust、Java、TypeScript 等)用他们选择的语言编写 eBPF 的用户态程序,而不需要学习新的语言;
  • 敏捷性:对于大型的 eBPF 应用程序,可以使用 WASM 作为插件扩展平台:扩展程序可以在运行时直接从控制平面交付和重新加载。这不仅意味着每个人都可以使用官方和未经修改的应用程序来加载自定义扩展,而且任何 eBPF 程序的错误修复和/或更新都可以在运行时推送和/或测试,而不需要更新和/或重新部署一个新的二进制;
  • 轻量级:WebAssembly 微服务消耗 1% 的资源,与 Linux 容器应用相比,冷启动的时间是 1%:我们也许可以借此实现 eBPF as a service,让 eBPF 程序的加载和执行变得更加轻量级、快速、简便易行;

eunomia-bpf 是 eBPF 技术探索 SIG [3] [5] 中发起并孵化的项目,目前也已经在 github [4] 上开源。eunomia-bpf 是一个 eBPF 程序的轻量级开发加载框架,包含了一个用户态动态加载框架/运行时库,以及一个简单的编译 WASM 和 eBPF 字节码的工具链容器。事实上,在 WASM 模块中编写 eBPF 代码和通常熟悉的使用 libbpf 框架或 Coolbpf 开发 eBPF 程序的方式是基本一样的,WASM 的复杂性会被隐藏在 eunomia-bpf 的编译工具链和运行时库中,开发者可以专注于 eBPF 程序的开发和调试,不需要了解 WASM 的背景知识,也不需要担心 WASM 的编译环境配置。

使用 WASM 模块分发、动态加载 eBPF 程序

eunomia-bpf 库包含一个简单的命令行工具(ecli),包含了一个小型的 WASM 运行时模块和 eBPF 动态装载的功能,可以直接下载下来后进行使用:

# download the release from https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecli
$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
$ sudo ./ecli run https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm
2022-10-11 14:05:50 URL:https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm [70076/70076] -> "/tmp/ebpm/app.wasm" [1]
running and waiting for the ebpf events from perf event...
{"pid":1709490,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1712603,"tpid":1717412,"sig":2,"ret":0,"comm":"kworker/u4:3","sig_name":"SIGINT"}
{"pid":1712603,"tpid":1717411,"sig":2,"ret":0,"comm":"kworker/u4:3","sig_name":"SIGINT"}
{"pid":0,"tpid":847,"sig":14,"ret":0,"comm":"swapper/1","sig_name":"SIGALRM"}
{"pid":1709490,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1709139,"tpid":1709077,"sig":0,"ret":0,"comm":"node","sig_name":"N/A"}
{"pid":1717420,"tpid":1717419,"sig":17,"ret":0,"comm":"cat","sig_name":"SIGCHLD"}

ecli 会自动从网页上下载并加载 sigsnoop/app.wasm 这个 wasm 模块,它包含了一个 eBPF 程序,用于跟踪内核中进程的信号发送和接收。这里我们可以看到一个简单的 JSON 格式的输出,包含了进程的 PID、信号的类型、发送者和接收者,以及信号名称等信息。它也可以附带一些命令行参数,例如:

$ wget https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/app.wasm
2022-10-11 14:08:07 (40.5 MB/s) - ‘app.wasm.1’ saved [70076/70076]

$ sudo ./ecli run app.wasm -h
Usage: sigsnoop [-h] [-x] [-k] [-n] [-p PID] [-s SIGNAL]
Trace standard and real-time signals.


    -h, --help  show this help message and exit
    -x, --failed  failed signals only
    -k, --killed  kill only
    -p, --pid=<int>  target pid
    -s, --signal=<int>  target signal

$ sudo ./ecli run app.wasm -p 1641
running and waiting for the ebpf events from perf event...
{"pid":1641,"tpid":14900,"sig":23,"ret":0,"comm":"YDLive","sig_name":"SIGURG"}
{"pid":1641,"tpid":14900,"sig":23,"ret":0,"comm":"YDLive","sig_name":"SIGURG"}

我们可以通过 -p 控制它追踪哪个进程,在内核态 eBPF 程序中进行一些过滤和处理。同样也可以使用 ecli 来动态加载使用其他的工具,例如 opensnoop:

$ sudo ./ecli run https://eunomia-bpf.github.io/eunomia-bpf/opensnoop/app.wasm
2022-10-11 14:11:56 URL:https://eunomia-bpf.github.io/eunomia-bpf/opensnoop/app.wasm [61274/61274] -> "/tmp/ebpm/app.wasm" [1]
running and waiting for the ebpf events from perf event...
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/1718823/cmdline"}
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/1718824/cmdline"}
{"ts":0,"pid":2344,"uid":0,"ret":26,"flags":0,"comm":"YDService","fname":"/proc/self/stat"}

opensnoop 会追踪进程的 open() 调用,即内核中所有的打开文件操作,这里我们可以看到进程的 PID、UID、返回值、调用标志、进程名和文件名等信息。内核态的 eBPF 程序会被包含在 WASM 模块中进行分发,在加载的时候通过 BTF 信息和 libbpf 进行重定位操作,以适应不同的内核版本。同时,由于用户态的相关处理代码完全由 WASM 编写,内核态由 eBPF 指令编写,因此不受具体指令集(x86、ARM 等)的限制,可以在不同的平台上运行。

使用 WASM 开发和打包 eBPF 程序

同样,以上文所述的 sigsnoop 为例,要跟踪进程的信号发送和接收,我们首先需要在 sigsnoop.bpf.c 中编写内核态的 eBPF 代码:

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include "sigsnoop.h"

const volatile pid_t filtered_pid = 0;
.....

struct {
 __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
 __uint(key_size, sizeof(__u32));
 __uint(value_size, sizeof(__u32));
} events SEC(".maps");

SEC("tracepoint/signal/signal_generate")
int sig_trace(struct trace_event_raw_signal_generate *ctx)
{
 struct event event = {};
 pid_t tpid = ctx->pid;
 int ret = ctx->errno;
 int sig = ctx->sig;
 __u64 pid_tgid;
 __u32 pid;

 ...
 pid_tgid = bpf_get_current_pid_tgid();
 pid = pid_tgid >> 32;
 if (filtered_pid && pid != filtered_pid)
  return 0;

 event.pid = pid;
 event.tpid = tpid;
 event.sig = sig;
 event.ret = ret;
 bpf_get_current_comm(event.comm, sizeof(event.comm));
 bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
 return 0;
}

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

这里我们使用 tracepoint/signal/signal_generate 这个 tracepoint 来在内核中追踪信号的产生事件。内核态代码通过 BPF_MAP_TYPE_PERF_EVENT_ARRAY 往用户态导出信息,为此我们需要在 sigsnoop.h 头文件,中定义一个导出信息的结构体:

#ifndef __SIGSNOOP_H
#define __SIGSNOOP_H

#define TASK_COMM_LEN 16

struct event {
 unsigned int pid;
 unsigned int tpid;
 int sig;
 int ret;
 char comm[TASK_COMM_LEN];
};

#endif /* __SIGSNOOP_H */

可以直接使用 eunomia-bpf 的编译工具链将其编译为 JSON 格式,生成一个 package.json 文件,并且可以直接使用 ecli 加载运行:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest
make
  BPF      .output/client.bpf.o
  GEN-SKEL .output/client.skel.h
  CC       .output/client.o
  CC       .output/cJSON.o
  CC       .output/create_skel_json.o
  BINARY   client
  DUMP_LLVM_MEMORY_LAYOUT
  DUMP_EBPF_PROGRAM
  FIX_TYPE_INFO_IN_EBPF
  GENERATE_PACKAGE_JSON

$ sudo ./ecli run package.json
running and waiting for the ebpf events from perf event...
time pid tpid sig ret comm
14:39:39 1723835 1723834 17 0 dirname
14:39:39 1723836 1723834 17 0 chmod
14:39:39 1723838 1723837 17 0 ps
14:39:39 1723839 1723837 17 0 grep
14:39:39 1723840 1723837 17 0 grep
14:39:39 1723841 1723837 17 0 wc

我们所有的编译工具链都已经打包成了 docker 镜像的形式并发布到了 docker hub 上,可以直接开箱即用。此时动态加载运行的只有内核态的 eBPF 代码和一些辅助信息,帮助 eunomia-bpf 库自动获取内核态往用户态上报的事件。如果我们想要在用户态进行一些参数配置和调整,以及数据处理流程,我们需要在用户态编写代码,将内核态的 eBPF 代码和用户态的代码打包成一个完整的 eBPF 程序。

可以直接一行命令,生成 eBPF 程序的用户态 WebAssembly 开发框架:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest gen-wasm-skel
make
  GENERATE_PACKAGE_JSON
  GEN-WASM-SKEL
$ ls
app.c eunomia-include ewasm-skel.h package.json README.md  sigsnoop.bpf.c  sigsnoop.h

我们提供的是 C 语言版本的 WASM 开发框架,它包含如下这些文件:

  • ewasm-skel.h:用户态 WebAssembly 开发框架的头文件,包含了预编译的 eBPF 程序字节码,和 eBPF 程序框架辅助信息,用来动态加载;
  • eunomia-include:一些 header-only 的库函数和辅助文件,用来辅助开发;
  • app.c:用户态 WebAssembly 程序的主要代码,包含了 eBPF 程序的主要逻辑,以及 eBPF 程序的数据处理流程。

以 sigsnoop 为例,用户态包含一些命令行解析、配置 eBPF 程序和数据处理的代码,会将根据 signal number 将信号事件的英文名称添加到事件中:

....
int main(int argc, const char** argv)
{
  struct argparse_option options[] = {
        OPT_HELP(),
        OPT_BOOLEAN('x', "failed", &failed_only, "failed signals only", NULL, 0, 0),
        OPT_BOOLEAN('k', "killed", &kill_only, "kill only", NULL, 0, 0),
        OPT_INTEGER('p', "pid", &target_pid, "target pid", NULL, 0, 0),
  OPT_INTEGER('s', "signal", &target_signal, "target signal", NULL, 0, 0),
        OPT_END(),
    };

  struct argparse argparse;
  argparse_init(&argparse, options, usages, 0);
  argparse_describe(&argparse, "Trace standard and real-time signals.\n", "");
  argc = argparse_parse(&argparse, argc, argv);

  cJSON *program = cJSON_Parse(program_data);
  program = set_bpf_program_global_var(program, "filtered_pid", cJSON_CreateNumber(target_pid));
  program = set_bpf_program_global_var(program, "target_signal", cJSON_CreateNumber(target_signal));
  program = set_bpf_program_global_var(program, "failed_only", cJSON_CreateBool(failed_only));
  return start_bpf_program(cJSON_PrintUnformatted(program));
}

int process_event(int ctx, char *e, int str_len)
{
 cJSON *json = cJSON_Parse(e);
 int sig = cJSON_GetObjectItem(json, "sig")->valueint;
 const char *name = sig_name[sig];
 cJSON_AddItemToObject(json, "sig_name", cJSON_CreateString(name));
 char *out = cJSON_PrintUnformatted(json);
 printf("%s\n", out);
 return 0;
}

最后使用容器镜像即可一行命令完成 WebAssembly/eBPF 程序的编译和打包,使用 ecli 即可一键运行:

$ docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest build-wasm
make
  GENERATE_PACKAGE_JSON
  BUILD-WASM
build app.wasm success
$ sudo ./ecli run app.wasm -h
Usage: sigsnoop [-h] [-x] [-k] [-n] [-p PID] [-s SIGNAL]

由于我们基于一次编译、到处运行的 libbpf 框架完成加载和启动 eBPF 程序的操作,因此编译和运行两个步骤是完全分离的,可以通过网络或任意方式直接进行 eBPF 程序的分发和部署,不依赖于特定内核版本。借助 WebAssembly 的轻量级特性,eBPF 程序的启动速度也比通常的使用镜像形式分发的 libbpf 程序快上不少,通常只需不到 100 ms 的时间即可完成,比起使用 BCC 部署启动时,使用 LLVM、Clang 编译运行消耗的时间和大量资源,更是有了质的飞跃。

上面提及的示例程序的完整代码,可以参考这里[6]。

演示视频

我们也有一个在 B 站上的演示视频,演示了如何从 bcc/libbpf-tools 中移植一个 eBPF 工具程序到 eunomia-bpf 中,并且使用 WASM 或 JSON 文件来分发、加载 eBPF 程序:https://www.bilibili.com/video/BV1JN4y1A76k

我们是如何做到的

ecli 是基于我们底层的 eunomia-bpf 库和运行时实现的一个简单的命令行工具。我们的项目架构如下图所示:

arch

ecli 工具基于 ewasm 库实现,ewasm 库包含一个 WAMR(wasm-micro-runtime) 运行时,以及基于 libbpf 库构建的 eBPF 动态装载模块。大致来说,我们在 WASM 运行时和用户态的 libbpf 中间多加了一层抽象层(eunomia-bpf 库),使得一次编译、到处运行的 eBPF 代码可以从 JSON 对象中动态加载。JSON 对象会在编译时被包含在 WASM 模块中,因此在运行时,我们可以通过解析 JSON 对象来获取 eBPF 程序的信息,然后动态加载 eBPF 程序。

使用 WASM 或 JSON 编译分发 eBPF 程序的流程图大致如下:

flow

大致来说,整个 eBPF 程序的编写和加载分为三个部分:

  1. 用 eunomia-cc 工具链将内核的 eBPF 代码骨架和字节码编译为 JSON 格式
  2. 在用户态开发的高级语言(例如 C 语言)中嵌入 JSON 数据,并提供一些 API 用于操作 JSON 形态的 eBPF 程序骨架
  3. 将用户态程序和 JSON 数据一起编译为 WASM 字节码并打包为 WASM 模块,然后在目标机器上加载并运行 WASM 程序
  4. 从 WASM 模块中加载内嵌的 JSON 数据,用 eunomia-bpf 库动态装载和配置 eBPF 程序骨架。

我们需要完成的仅仅是少量的 native API 和 WASM 运行时的绑定,并且在 WASM 代码中处理 JSON 数据。你可以在一个单一的 WASM 模块中拥有多个 eBPF 程序。如果不使用我们提供的 WASM 运行时,或者想要使用其他语言进行用户态的 eBPF 辅助代码的开发,在我们提供的 eunomia-bpf 库基础上完成一些 WebaAssembly 的绑定即可。

另外,对于 eunomia-bpf 库而言,不需要 WASM 模块和运行时同样可以启动和动态加载 eBPF 程序,不过此时动态加载运行的就只是内核态的 eBPF 程序字节码。你可以手动或使用任意语言修改 JSON 对象来控制 eBPF 程序的加载和参数,并且通过 eunomia-bpf 自动获取内核态上报的返回数据。对于初学者而言,这可能比使用 WebAssembly 更加简单方便:只需要编写内核态的 eBPF 程序,然后使用 eunomia-cc 工具链将其编译为 JSON 格式,最后使用 eunomia-bpf 库加载和运行即可。完全不用考虑任何用户态的辅助程序,包括 WASM 在内。具体可以参考我们的使用手册[7]或示例代码[8]。

未来的方向

目前 eunomia-bpf 的工具链的实现还远远谈不上完善,只是有一个可行性验证的版本。对于一个开发工具链来说,具体的 API 标准和相关的生态是非常重要的,我们希望如果有机会的话,也许可以和 SIG 社区的其他成员一起讨论并形成一个具体的 API 标准,能够基于 eBPF 和 WASM 等技术,共同提供一个通用的、跨平台和内核版本的插件生态,为各自的应用增加 eBPF 和 WASM 的超能力。

目前 eunomia-bpf 跨内核版本的动态加载特性还依赖于内核的 BTF 信息,SIG 社区的 Coolbpf 项目[9]本身能提供 BTF 的自动生成、低版本内核的适配功能,未来低版本内核的支持会基于 Coolbpf 的现有的部分完成。同时,我们也会给 Coolbpf 的 API 实现、远程编译后端提供类似于 eunomia-bpf 的内核态编译和运行完全分离的功能,让使用 Coolbpf API 开发 eBPF 的程序,在远程编译一次过后可以在任意内核版本和架构上直接使用,在部署时无需再次连接远程服务器;也可以将编译完成的 eBPF 程序作为 Go、Python、Rust 等语言的开发包直接使用,让开发者能轻松获得 eBPF 程序上报的信息,而完全不需要再次进行任何 eBPF 程序的编译过程。

SIG 社区孵化于高校的 Linux Microscope (LMP) 项目[10]中,也已经有一些基于 eunomia-bpf 提供通用的、规范化、可以随时下载运行的 eBPF 程序或工具库的计划,目前还在继续完善的阶段。

参考资料

  1. eBPF 和 WebAssembly:哪种 VM 会制霸云原生时代? https://juejin.cn/post/7043721713602789407
  2. eBPF 和 Wasm:探索服务网格数据平面的未来: https://cloudnative.to/blog/ebpf-wasm-service-mesh/
  3. eBPF 技术探索 SIG 主页: https://openanolis.cn/sig/ebpfresearch
  4. eunomia-bpf Github 仓库:https://github.com/eunomia-bpf/eunomia-bpf
  5. eunomia-bpf 龙蜥社区镜像仓库:https://gitee.com/anolis/eunomia
  6. sigsnoop 示例代码:https://gitee.com/anolis/eunomia/tree/master/examples/bpftools/sigsnoop
  7. eunomia-bpf 用户手册:https://openanolis.cn/sig/ebpfresearch/doc/646023027267993641
  8. 更多示例代码:https://gitee.com/anolis/eunomia/tree/master/examples/bpftools/sigsnoop
  9. Coolbpf 项目介绍:https://openanolis.cn/sig/ebpfresearch/doc/633529753894377555
  10. LMP 项目介绍:https://openanolis.cn/sig/ebpfresearch/doc/633661297090877527

#! https://zhuanlan.zhihu.com/p/589784489

如何在 Linux 显微镜(LMP)项目中开启 eBPF 之旅?

eBPF 为 Linux 内核提供了可扩展性,使开发人员能够对 Linux 内核进行编程,以便根据他们的业务需求快速构建智能的或丰富的功能。

我们的 LMP(Linux Microscope) 项目 是为了充分挖掘 ebpf 的可能性而建立的,项目以构建 eBPF 学习社区、成为 eBPF 工具集散地、孵化 eBPF 想法和项目为目标,正在大力建设中。之前我们在 LMP 其中的 eBPF Supermarket 中包含了大量由个人开发者编写的 eBPF 工具,覆盖了网络、性能分析、安全等多种功能,我们正在尝试把其中的一些程序迁移到 eBPF Hub,一些规范化的 eBPF 程序库,可以随时下载运行,或嵌入大型应用程序中作为插件使用。

我们尝试在 eBPF Hub 中,基于 eunomia-bpf 开发框架创建符合 OCI 标准的 WASM 和 eBPF 程序,并利用 ORAS 简化扩展 LMP 的 eBPF 分发、加载、运行能力。

快速使用

如果您想快速开始 eBPF,可以使用我们开发的轻量级框架之上的命令行程序 lmp-cli。当使用脚本安装好我们的框架之后,您只需要一条命令,无需任何编译,即可体会到 eBPF 的强大之处:

$ lmp run sigsnoop
download with curl: https://linuxkerneltravel.github.io/lmp/sigsnoop/package.json
running and waiting for the eBPF events from perf event...
time pid tpid sig ret comm
00:21:41 109955 112863 28 0 gnome-terminal-
00:21:41 109955 112862 28 0 gnome-terminal-
...

如果您使用过 bcc 等 eBPF 开发工具,您一定会惊喜于 LMP 的便捷性。LMP 中包含了各种各样的 eBPF 程序,这种便捷的运行,离不开我们基于的底层框架 eunomia-bpf,它完全实现了“一次编译,处处运行”的 eBPF 跨平台目标。在 eunomia-bpf 框架下,LMP 开发的 eBPF 应用不仅可以适配任意架构和不同内核版本,而且还具有轻量级、良好的隔离性等优点,可以作为插件到嵌入大型应用之中。

eunomia-bpf:结合 eBPF 和 WASM 的轻量级开发框架

作为一个 eBPF 程序的轻量级开发加载框架,eunomia-bpf 基于 WASM 运行时和 BTF 技术,包含了一个用户态动态加载框架/运行时库,以及一个简单的编译 WASM 和 eBPF 字节码的工具链容器。

Wasm 是为了一个可移植的目标而设计的,可作为 C/C+/RUST 等高级语言的编译目标,使客户端和服务器应用程序能够在 Web 上部署。目前已经发展成为一个轻量级、高性能、跨平台和多语种的软件沙盒环境,被运用于云原生软件组件。 eunomia-bpf 将 eBPF 用户态的所有控制和数据处理逻辑全部移到 WASM 虚拟机中,通过 WASM module 打包和分发 eBPF 字节码,同时在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,将二者的优势结合了起来。

在 WASM 模块中编写 eBPF 代码和通常熟悉的使用 libbpf 框架或 Coolbpf 开发 eBPF 程序的方式是基本一样的,WASM 的复杂性会被隐藏在 eunomia-bpf 的编译工具链和运行时库中,开发者可以专注于 eBPF 程序的开发和调试,不需要了解 WASM 的背景知识,也不需要担心 WASM 的编译环境配置。

大致来说,eunomia-bpf 在 WASM 运行时和用户态的 libbpf 中间多加了一层抽象层,使得一次编译、到处运行的 eBPF 代码可以从 JSON 对象中动态加载。JSON 对象会在编译时被包含在 WASM 模块中,因此在运行时,我们可以通过解析 JSON 对象来获取 eBPF 程序的信息,然后动态加载 eBPF 程序。通过 WASM module 打包和分发 eBPF 字节码,同时在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,eunomia-bpf 就可以将二者的优势结合起来,让任意 eBPF 程序能有如下特性:

  • 可移植:让 eBPF 工具和应用不需要进行重新编译即可以跨平台分发,省去了复杂的交叉编译流程;
  • 隔离性:让 eBPF 程序的加载和执行、以及用户态的数据处理流程更加安全可靠。
  • 包管理:完成 eBPF 程序或工具的分发、管理、加载等工作。
  • 敏捷性:使每个人都可以使用官方和未经修改的应用程序来加载自定义扩展,任何 eBPF 程序的错误修复和/或更新都可以在运行时推送和/或测试,而不需要更新和/或重新部署一个新的二进制。
  • 轻量级:与 Linux 容器应用相比,WASM 微服务冷启动的时间是 1%,可以实现 eBPF as a service,让 eBPF 程序的加载和执行变得更加轻量级、快速、简便易行。

我们已经测试了在 x86、ARM 等不同架构不同内核版本的 Linux 系统上,eunomia-bpf 框架都可以使用同一个预编译 eBPF 程序二进制,从云端一行命令获取到本地之后运行。之后 eunomia-bpf 还会添加 RISC-V 等更多架构的支持。

使用 lmp-cli 构建一个 eBPF 项目

如果您是一个 eBPF 工具的使用者,您可以无需任何编译流程,也不需要了解任何 eBPF 和 WASM 的相关知识,使用 lmp run <name> 就可以直接运行 LMP 仓库的小程序,其中会调用lmp pull <name>命令从云端从库中下载对应的小程序。

如果您是一个 eBPF 程序的开发者,让我们开始创建、编译并运行一个简单的程序。在这里,我们使用基简单命令行工具 lmp-cli,概述如何从四个步骤开始构建。

1. 准备你的环境

eBPF 本身是一种 Linux 内核技术,因此任何实际的 BPF 程序都必须在 Linux 内核中运行。我建议您从内核 5.4 或更新的版本开始。从 SSH 终端,检查内核版本,并确认您已经启用了 CONFIG_DEBUG_INFO_BTF:

uname -r
cat /boot/config-$(uname -r) | grep CONFIG_DEBUG_INFO_BTF

你会看到类似这样的输出:

$ uname -r
5.15.0-48-generic

$ cat /boot/config-$(uname -r) | grep CONFIG_DEBUG_INFO_BTF
CONFIG_DEBUG_INFO_BTF=y
CONFIG_DEBUG_INFO_BTF_MODULES=y

安装命令行工具 lmp-cli:

curl https://github.com/GorilaMond/lmp_cli/releases/download/lmp/install.sh | sh

2. 创建项目的内核部分

使用lmp init创建一个项目模板,来初始化你的内核程序,快速地投入到代码的编写中:

lmp init hello

成功创建项目后,您将看到如下类似的输出:

$ lmp init hello
Cloning into 'ebpm-template'...

它实际上创建了一个项目名对应的文件夹,里面有这些文件:

$ cd hello/
$ ll
...
-rw-rw-r--  1 a a 2910 10月 17 23:18 bootstrap.bpf.c
-rw-rw-r--  1 a a  392 10月 17 23:18 bootstrap.h
-rw-rw-r--  1 a a  221 10月 17 23:18 config.json
drwxrwxr-x  8 a a 4096 10月 17 23:18 .git/
drwxrwxr-x  3 a a 4096 10月 17 23:18 .github/
-rw-rw-r--  1 a a   21 10月 17 23:18 .gitignore
-rw-rw-r--  1 a a 2400 10月 17 23:18 README.md

内核程序模板 bootstrap.bpf.c 中默认的跟踪点为 tp/sched/sched_process_exectp/sched/sched_process_exit,用来跟踪新程序的执行和退出,这里不做修改。

构建内核项目,如下所示。保存您的更改,使用 sudo lmp build 构建内核程序,这会创建一个名为 package.json 的对象文件。

$ sudo lmp build
make
  ...
  BINARY   client
  DUMP_LLVM_MEMORY_LAYOUT
  DUMP_EBPF_PROGRAM
  FIX_TYPE_INFO_IN_EBPF
  GENERATE_PACKAGE_JSON

3. 运行内核程序

可以使用lmp run package.json运行内核程序,没有用户端程序对数据的处理的情况下,该框架下内核程序将会输出所有被 output 的数据:

$ sudo lmp run ./package.json
running and waiting for the ebpf events from ring buffer...
time pid ppid exit_code duration_ns comm filename exit_event

一开始您不会看到任何数据,只有当内核的跟踪点被触发时,这里是新的进程被创建或退出时,才会输出数据。这里新建了一个虚拟终端,输出了如下数据:

23:31:31 111788 109955 0 0 bash /bin/bash 0
23:31:31 111790 111788 0 0 lesspipe /usr/bin/lesspipe 0
...

4. 添加用户态程序

我们提供的是 demo 是 C 语言版本的 WASM 开发框架,在构建好的内核项目文件夹内,使用 sudo lmp gen-wasm-skel 生成一个 WASM 用户态项目模板,app.c、eunomia-include、ewasm-skel.h 这些文件会被生成。ewasm-skel.h 是被打包为头文件的内核程序,app.c 是用户态程序的模板文件,我们可以修改它来进行自定义的数据处理,这里不做修改。

$ sudo lmp gen-wasm-skel
make
  BPF      .output/client.bpf.o
...

使用sudo lmp build-wasm构建用户态程序,生成 app.wasm 文件

$ sudo lmp build-wasm
make
  BPF      .output/client.bpf.o
...

使用lmp run app.wasm运行用户态程序,json 格式的输出为通用的数据处理做好了准备:

$ lmp run app.wasm
running and waiting for the ebpf events from ring buffer...
{"pid":112665,"ppid":109955,"exit_code":0,"duration_ns":0,"comm":"bash","filename":"/bin/bash","exit_event":0}
{"pid":112667,"ppid":112665,"exit_code":0,"duration_ns":0,"comm":"lesspipe","filename":"/usr/bin/lesspipe","exit_event":0}
{"pid":112668,"ppid":112667,"exit_code":0,"duration_ns":0,"comm":"basename","filename":"/usr/bin/basename","exit_event":0}
...

另一个例子:使用 eBPF 打印进程内存使用状况

可以将 bootstrap.bpf.c 重命名为 procstat.bpf.c,将 bootstrap.h 重命名为 procstat.h,然后编译运行。对应的源代码如下:

procstat.bpf.c

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "procstat.h"

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

struct {
 __uint(type, BPF_MAP_TYPE_RINGBUF);
 __uint(max_entries, 256 * 1024);
} rb SEC(".maps");


SEC("kprobe/finish_task_switch")
int BPF_KPROBE(finish_task_switch, struct task_struct *prev)
{
 struct event *e;
 struct mm_rss_stat rss = {};
 struct mm_struct *mms;
 long long *t;

 e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
 if (!e)
  return 0;

 e->pid = BPF_CORE_READ(prev, pid);
 e->vsize = BPF_CORE_READ(prev, mm, total_vm);
 e->Vdata = BPF_CORE_READ(prev, mm, data_vm);
 e->Vstk = BPF_CORE_READ(prev, mm, stack_vm);
 e->nvcsw = BPF_CORE_READ(prev, nvcsw);
 e->nivcsw = BPF_CORE_READ(prev, nivcsw);

 rss = BPF_CORE_READ(prev, mm, rss_stat);
 t = (long long *)(rss.count);
 e->rssfile = *t;
 e->rssanon = *(t + 1);
 e->vswap = *(t + 2);
 e->rssshmem = *(t + 3);
 e->size = *t + *(t + 1) + *(t + 3);

 bpf_ringbuf_submit(e, 0);
 return 0;
}

proc.h

#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
/*进程内存状态报告*/
    pid_t pid;
    long nvcsw;
    long nivcsw;
    long vsize;              //虚拟内存
    long size;               //物理内存
    long long rssanon;       //匿名页面
    long long rssfile;       //文件页面
    long long rssshmem;      //共享页面
    long long vswap;         //交换页面
    long long Hpages;        //hugetlbPages
    long Vdata;              //Private data segments
    long Vstk;               //User stack
    long long VPTE;
};
#endif /* __BOOTSTRAP_H */

具体的上报事件信息在 event 结构体中定义:

参数含义
vsize进程使用的虚拟内存
size进程使用的最大物理内存
rssanon进程使用的匿名页面
rssfile进程使用的文件映射页面
rssshmem进程使用的共享内存页面
vswap进程使用的交换分区大小
vdata进程使用的私有数据段大小
vpte进程页表大小
vstk进程用户栈大小

挂载点与挂载原因分析:

  • 首先,获取进程级别内存使用信息需要获取到进程的 task_struct 结构体,其中在 mm_struct 成员中存在一个保存进程当前内存使用状态的数组结构,因此有关进程的大部分内存使用信息都可以通过这个数组获得。
  • 其次,需要注意函数的插入点,插入点的选取关系到数据准确性是否得到保证,而在进程的内存申请,释放,规整等代码路径上都存在页面状态改变,但是数量信息还没有更新的相关结构中的情况,如果插入点这两者中间,数据就会和实际情况存在差异,所有在确保可以获取到进程 PCB 的前提下,选择在进程调度代码路径上考虑。而 finish_task_switch 函数是新一个进程第一个执行的函数,做的事却是给上一个被调度出去的进程做收尾工作,所以这个函数的参数是上一个进程的 PCB,从这块获得上一个进程的内存信息就可以确保在它没有再次被调度上 CPU 执行的这段时间内的内存数据稳定性。
  • 因此最后选择将程序挂载到 finish_task_switch 函数上。数据来源有两部分,一个是 mm_struc 结构本身存在的状态信息,另一个是在 mm_rss_stat 结构中。

也可以在 bolipi 的平台中在线编译,在线体验运行 eBPF 程序:https://bolipi.com/ebpf/home/online

完整的代码、文档和运行结果可以在 LMP 中 eBPF_Supermarket 处找到:eBPF_Supermarket/Memory_Subsystem/memstat/procstat

相关背景

LMP 项目的成立初衷是:

  • 面向 eBPF 初学者和爱好者,提供 eBPF 学习资料、程序/项目案例,构建 eBPF 学习社区
  • 成为 eBPF 工具集散地,我们相信每一位 eBPF 初学者和爱好者都有无限的创造力
  • 孵化 eBPF 想法、相关工具、项目

LMP 目前分为四个子项目:

  • eBPF_Supermarket 中包含了大量由个人开发者编写的 eBPF 工具,覆盖了网络、性能分析、安全等多种功能;
  • eBPF_Hub 是规范化的 eBPF 程序库,可以随时下载运行;
  • eBPF_Visualization 是为 eBPF 程序管理而开发的 web 管理系统,聚焦 eBPF 数据可视化和内核可视化;
  • eBPF_Documentation 为社区收集、梳理和原创的 eBPF 相关资料和文档。

当前 LMP 项目也存在一些问题,例如对于 eBPF 工具的开发者,存在非常多而且复杂的用户态可视化、展示方案,有许多套系统提供可视化的实现并且有多种语言混合,缺乏展示标准、也难以进行可视化的整合等。因此,我们希望尝试借助 eunomia-bpf 提供的符合 OCI 标准的 WASM 和 eBPF 程序,提供标准化、高可扩展性的基于 eBPF 的可视化、数据展示、分析平台,利用 ORAS 简化扩展 eBPF 的分发、加载、运行能力,为 eBPF 工具的开发者和使用者提供更加简单、高效的体验。

WebAssembly

WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 c\c++ 等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。而且,更棒的是,这是通过 W3C WebAssembly Community Group 开发的一项网络标准,并得到了来自各大主要浏览器厂商的积极参与。

尽管 WebAssembly 是为运行在 Web 上设计的,它也可以在其它的环境中良好地运行。包括从用作测试的最小化 shell ,到完全的应用环境 —— 例如:在数据中心的服务器、物联网(IoT)设备或者是移动/桌面应用程序。甚至,运行嵌入在较大程序里的 WebAssembly 也是可行的。通常,通过维持不需要 Web API 的非 Web 路径,WebAssembly 能够在许多平台上用作便携式的二进制格式,为移植性、工具和语言无关性带来巨大的好处。(因为它支持 c\c++ 级语义)

WASM 的编译和部署流程如下:

xxx
wasm-compile-deploy

OCI(Open Container Initiative)

开放容器协议(OCI)是一个轻量级,开放的治理结构,为容器技术定义了规范和标准。在 Linux 基金会的支持下成立,由各大软件企业构成,致力于围绕容器格式和运行时创建开放的行业标准。其中包括了使用 Container Registries 进行工作的 API,正式名称为 OCI 分发规范(又名“distribution-spec”)。这个发布规范是基于 Docker 公司最初发布的开源注册服务器编写的,它存在于 GitHub 的distribution/distribution(现在是CNCF项目)上。

OCI 目前提出的规范有如下这些:

其中 runtime 和 image 的规范都已经正式发布,而 distribution 的还在工作之中。runtime 规范中介绍了如何运行解压缩到磁盘上的 Filesystem Bundle。在 OCI 标准下,运行一个容器的过程就是下载一个 OCI 的镜像,将其解压到某个 Filesystem Bundle 中,然后某个 OCI Runtime 就会运行这个 Bundle。

伴随着 image spec 与 distribution spec 的演化,人们开始逐步认识到除了 Container Images 之外,Registries 还能够用来分发 Kubernetes Deployment Files, Helm Charts, docker-compose, CNAB 等产物。它们可以共用同一套 API,同一套存储,将 Registries 作为一个云存储系统。这就为带来了 OCI Artifacts 的概念,用户能够把所有的产物都存储在 OCI 兼容的 Registiry 当中并进行分发。为此,Microsoft 将 oras 作为一个 client 端实现捐赠给了社区,包括 Harbor 在内的多个项目都在积极的参与。

ORAS(OCI Registry As Storage)

Registries 正在逐渐演变为通用的组件存储库。为了实现这一目标,ORAS 项目提供了一种将 OCI Artifacts 从 OCI Registries 提交和拉取的方法。正在寻求通用 Registries 客户端的用户可以从ORAS CLI中得到帮助,而开发人员可以在ORAS 客户端的开发库之上构建自己的客户端。

ORAS 的工作原理与您可能已经熟悉的工具(如 docker)类似。它允许您向 OCI Registries 推送(上传)和提取(下载)内容,并处理登录(身份验证)和令牌流(授权)。ORAS 的不同之处在于将焦点从容器映像转移到其他类型的组件上。

因此,鼓励新的 OCI Artifacts 的作者定义他们自己的组件媒体类型,以使得他们的用户知道如何对其进行操作。

如果您希望立即开始发布 OCI Artifacts,请查看ORAS CLI。希望提供给自己用户体验的开发人员应该使用一个 ORAS 客户端开发库。

未来的发展方向

未来 LMP 会专注于更多的基于 eBPF 的应用工具和实践的开发:

  1. 进一步完善 ORAS 和 OCI 镜像相关的支持;
  2. 重构并迁移现有的 eBPF 工具,提供完整的、开箱即用的分析工具组件,例如性能工程等方面;
  3. 探索和孵化更多的 eBPF 想法、相关工具、项目;

我们所基于的 eunomia-bpf 项目也会继续完善,专注于提供一个底层的 eBPF 开发平台和运行时基础设施,力求带来更好的开发和移植体验:

  1. 测试更多平台和内核版本的支持,目前已经在 ARM64x86_64 上成功移植并运行,接下来会对低内核版本、Android、RISC-V 等平台,以及嵌入式、边缘计算相关的设备进行更进一步的测试;也许在未来,我们还可以提供 Windows 上的 eBPF 程序支持和类似的开发体验;
  2. 提供标准化、稳定的 JSON 和 WASM 接口协议规范以及 OCI 镜像规范,不和任何的供应商或云服务绑定。如果不使用 eunomia-bpf 相关的底层运行时,或使用自定义的 WASM 运行时,也可以通过标准化的接口来使用 LMP 中已经有的大量 eBPF 程序生态。
  3. 提供更友好的用户态开发接口,以及更多的用户态开发语言 SDK,例如 Go、Rust、Python 等;
  4. 进行更多关于 WASM 和 eBPF 结合的探索;

参考资料 & 推荐阅读

#! https://zhuanlan.zhihu.com/p/589784295

eunomia-bpf 0.3.0 发布:只需编写内核态代码,轻松构建、打包、发布完整的 eBPF 应用

eunomia-bpf 简介

eBPF 源于 BPF,本质上是处于内核中的一个高效与灵活的虚拟机组件,以一种安全的方式在许多内核 hook 点执行字节码,开发者可基于 eBPF 开发性能分析工具、软件定义网络、安全等诸多场景。但是,目前对于开发和使用 eBPF 应用而言还可能存在一些不够方便的地方:

  • 搭建和开发 eBPF 程序是一个门槛比较高、比较复杂的工作,必须同时关注内核态和用户态两个方面的交互和信息处理,有时还要配置环境和编写对应的构建脚本;
  • 目前不同用户态语言如 C、Go、Rust 等编写的工具难以兼容、难以统一管理,多种开发生态难以整合:如何跨架构、跨语言和内核版本,使用标准化的方式方便又快捷的打包、分发、发布二进制 eBPF 程序,同时还需要能很方便地动态调整 eBPF 程序的挂载点、参数等等?
  • 如何更方便地使用 eBPF 的工具:有没有可能从云端一行命令拉下来就使用,类似 docker 那样?或者把 eBPF 程序作为服务运行,通过 HTTP 请求和 URL 即可热更新、动态插拔运行任意一个 eBPF 程序?

eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。

使用 eunomia-bpf ,可以:

  • 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息;
  • 使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;
  • eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。

eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpf, libbpfgo, libbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等。

我们发布了最新的 0.3 版本, 对于整体的开发和使用流程进行了优化,同时也支持了更多的 eBPF 程序和 maps 类型。

运行时优化:增强功能性, 增加多种程序类型

  1. 只需编写内核态代码, 即可获得对应的输出信息, 以可读、规整的方式打印到标准输出. 以一个简单的 eBPF 程序, 跟踪所有 open 类型系统调用的 opensnoop 为例:

    头文件 opensnoop.h

    #ifndef __OPENSNOOP_H
    #define __OPENSNOOP_H
    
    #define TASK_COMM_LEN 16
    #define NAME_MAX 255
    #define INVALID_UID ((uid_t)-1)
    
    // used for export event
    struct event {
      /* user terminology for pid: */
      unsigned long long ts;
      int pid;
      int uid;
      int ret;
      int flags;
      char comm[TASK_COMM_LEN];
      char fname[NAME_MAX];
    };
    
    #endif /* __OPENSNOOP_H */
    

    内核态代码 opensnoop.bpf.c

    #include <vmlinux.h>
    #include <bpf/bpf_helpers.h>
    #include "opensnoop.h"
    
    struct args_t {
      const char *fname;
      int flags;
    };
    
    /// Process ID to trace
    const volatile int pid_target = 0;
    /// Thread ID to trace
    const volatile int tgid_target = 0;
    /// @description User ID to trace
    const volatile int uid_target = 0;
    /// @cmdarg {"default": false, "short": "f", "long": "failed"}
    const volatile bool targ_failed = false;
    
    struct {
      __uint(type, BPF_MAP_TYPE_HASH);
      __uint(max_entries, 10240);
      __type(key, u32);
      __type(value, struct args_t);
    } start SEC(".maps");
    
    struct {
      __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
      __uint(key_size, sizeof(u32));
      __uint(value_size, sizeof(u32));
    } events SEC(".maps");
    
    static __always_inline bool valid_uid(uid_t uid) {
      return uid != INVALID_UID;
    }
    
    static __always_inline
    bool trace_allowed(u32 tgid, u32 pid)
    {
      u32 uid;
    
      /* filters */
      if (tgid_target && tgid_target != tgid)
        return false;
      if (pid_target && pid_target != pid)
        return false;
      if (valid_uid(uid_target)) {
        uid = (u32)bpf_get_current_uid_gid();
        if (uid_target != uid) {
          return false;
        }
      }
      return true;
    }
    
    SEC("tracepoint/syscalls/sys_enter_open")
    int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx)
    {
      u64 id = bpf_get_current_pid_tgid();
      /* use kernel terminology here for tgid/pid: */
      u32 tgid = id >> 32;
      u32 pid = id;
    
      /* store arg info for later lookup */
      if (trace_allowed(tgid, pid)) {
        struct args_t args = {};
        args.fname = (const char *)ctx->args[0];
        args.flags = (int)ctx->args[1];
        bpf_map_update_elem(&start, &pid, &args, 0);
      }
      return 0;
    }
    
    SEC("tracepoint/syscalls/sys_enter_openat")
    int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
    {
      u64 id = bpf_get_current_pid_tgid();
      /* use kernel terminology here for tgid/pid: */
      u32 tgid = id >> 32;
      u32 pid = id;
    
      /* store arg info for later lookup */
      if (trace_allowed(tgid, pid)) {
        struct args_t args = {};
        args.fname = (const char *)ctx->args[1];
        args.flags = (int)ctx->args[2];
        bpf_map_update_elem(&start, &pid, &args, 0);
      }
      return 0;
    }
    
    static __always_inline
    int trace_exit(struct trace_event_raw_sys_exit* ctx)
    {
      struct event event = {};
      struct args_t *ap;
      int ret;
      u32 pid = bpf_get_current_pid_tgid();
    
      ap = bpf_map_lookup_elem(&start, &pid);
      if (!ap)
        return 0; /* missed entry */
      ret = ctx->ret;
      if (targ_failed && ret >= 0)
        goto cleanup; /* want failed only */
    
      /* event data */
      event.pid = bpf_get_current_pid_tgid() >> 32;
      event.uid = bpf_get_current_uid_gid();
      bpf_get_current_comm(&event.comm, sizeof(event.comm));
      bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname);
      event.flags = ap->flags;
      event.ret = ret;
    
      /* emit event */
      bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
                &event, sizeof(event));
    
    cleanup:
      bpf_map_delete_elem(&start, &pid);
      return 0;
    }
    
    SEC("tracepoint/syscalls/sys_exit_open")
    int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx)
    {
      return trace_exit(ctx);
    }
    
    SEC("tracepoint/syscalls/sys_exit_openat")
    int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx)
    {
      return trace_exit(ctx);
    }
    
    /// Trace open family syscalls.
    char LICENSE[] SEC("license") = "GPL";
    

    编译运行:

    $ ecc opensnoop.bpf.c opensnoop.h
    Compiling bpf object...
    Generating export types...
    Packing ebpf object and config into package.json...
    $ sudo ecli examples/bpftools/opensnoop/package.json
    TIME     TS      PID     UID     RET     FLAGS   COMM    FNAME   
    20:31:50  0      1       0       51      524288  systemd /proc/614/cgroup
    20:31:50  0      33182   0       25      524288  ecli    /etc/localtime
    20:31:53  0      754     0       6       0       irqbalance /proc/interrupts
    20:31:53  0      754     0       6       0       irqbalance /proc/stat
    20:32:03  0      754     0       6       0       irqbalance /proc/interrupts
    20:32:03  0      754     0       6       0       irqbalance /proc/stat
    20:32:03  0      632     0       7       524288  vmtoolsd /etc/mtab
    20:32:03  0      632     0       9       0       vmtoolsd /proc/devices
    
    $ sudo ecli examples/bpftools/opensnoop/package.json --pid_target 754
    TIME     TS      PID     UID     RET     FLAGS   COMM    FNAME   
    20:34:13  0      754     0       6       0       irqbalance /proc/interrupts
    20:34:13  0      754     0       6       0       irqbalance /proc/stat
    20:34:23  0      754     0       6       0       irqbalance /proc/interrupts
    20:34:23  0      754     0       6       0       irqbalance /proc/stat
    

    或使用 docker 编译:

    docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest
    

    编译发布后, 也可以轻松从云端一行命令启动任意 eBPF 程序, 例如:

    wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli     # download the release from https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecli
    sudo ./ecli https://eunomia-bpf.github.io/eunomia-bpf/sigsnoop/package.json # simply run a pre-compiled ebpf code from a url
    sudo ./ecli sigsnoop:latest # run with a name and download the latest version bpf tool from our repo
    

    完整代码在这里: https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/opensnoop

  2. 支持根据代码中的注释信息自动生成用户态命令行参数。

    比如需要实现一个 ebpf 程序里面的 pid 过滤器,只需要编写内核态代码,在 eBPF 中声明全局变量,即可自动生成命令行参数:

    /// Process ID to trace
    const volatile pid_t pid_target = 0;
    /// Thread ID to trace
    const volatile pid_t tgid_target = 0;
    /// @description User ID to trace
    const volatile uid_t uid_target = 0;
    /// @cmdarg {"default": false, "short": "f", "long": "failed"}
    /// @description target pid to trace
    const volatile bool targ_failed = false;
    

    我们会将注释文档的描述信息提取,放在配置文件里面,并且变成 eBPF 应用的命令行参数. 使用方式以跟踪所有 open 系统调用的 opensnoop 为例:

    $ sudo ecli  examples/bpftools/opensnoop/package.json -h
    Usage: opensnoop_bpf [--help] [--version] [--verbose] [--pid_target VAR] [--tgid_target VAR] [--uid_target VAR] [--failed]
    
    Trace open family syscalls.
    
    Optional arguments:
      -h, --help    shows help message and exits 
      -v, --version prints version information and exits 
      --verbose     prints libbpf debug information 
      --pid_target  Process ID to trace 
      --tgid_target Thread ID to trace
    
    $ sudo ecli examples/bpftools/opensnoop/package.json --pid_target 754
    TIME     TS      PID     UID     RET     FLAGS   COMM    FNAME   
    20:34:13  0      754     0       6       0       irqbalance /proc/interrupts
    20:34:13  0      754     0       6       0       irqbalance /proc/stat
    20:34:23  0      754     0       6       0       irqbalance /proc/interrupts
    20:34:23  0      754     0       6       0       irqbalance /proc/stat
    
  3. 支持自动采集和综合非 ring buffer 和 perf event 的 map,比如 hash map,打印出信息或生成直方图。

    之前使用 ring buffer 和 perf event 的场景会稍微受限,因此需要有一种方法可以自动从 maps 里面采集数据,在源代码里面添加注释即可:

    /// @sample {"interval": 1000, "type" : "log2_hist"}
    struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __uint(max_entries, MAX_ENTRIES);
        __type(key, u32);
        __type(value, struct hist);
    } hists SEC(".maps");
    

    就会每隔一秒去采集一次 counters 里面的内容(print_map),以 runqlat 为例:

    $ sudo ecli examples/bpftools/runqlat/package.json -h
    Usage: runqlat_bpf [--help] [--version] [--verbose] [--filter_cg] [--targ_per_process] [--targ_per_thread] [--targ_per_pidns] [--targ_ms] [--targ_tgid VAR]
    
    Summarize run queue (scheduler) latency as a histogram.
    
    Optional arguments:
      -h, --help            shows help message and exits 
      -v, --version         prints version information and exits 
      --verbose             prints libbpf debug information 
      --filter_cg           set value of bool variable filter_cg 
      --targ_per_process    set value of bool variable targ_per_process 
      --targ_per_thread     set value of bool variable targ_per_thread 
      --targ_per_pidns      set value of bool variable targ_per_pidns 
      --targ_ms             set value of bool variable targ_ms 
      --targ_tgid           set value of pid_t variable targ_tgid 
    
    Built with eunomia-bpf framework.
    See https://github.com/eunomia-bpf/eunomia-bpf for more information.
    
    $ sudo ecli examples/bpftools/runqlat/package.json
    key =  4294967295
    comm = rcu_preempt
    
        (unit)              : count    distribution
            0 -> 1          : 9        |****                                    |
            2 -> 3          : 6        |**                                      |
            4 -> 7          : 12       |*****                                   |
            8 -> 15         : 28       |*************                           |
           16 -> 31         : 40       |*******************                     |
           32 -> 63         : 83       |****************************************|
           64 -> 127        : 57       |***************************             |
          128 -> 255        : 19       |*********                               |
          256 -> 511        : 11       |*****                                   |
          512 -> 1023       : 2        |                                        |
         1024 -> 2047       : 2        |                                        |
         2048 -> 4095       : 0        |                                        |
         4096 -> 8191       : 0        |                                        |
         8192 -> 16383      : 0        |                                        |
        16384 -> 32767      : 1        |                                        |
    
    $ sudo ecli examples/bpftools/runqlat/package.json --targ_per_process
    key =  3189
    comm = cpptools
    
        (unit)              : count    distribution
            0 -> 1          : 0        |                                        |
            2 -> 3          : 0        |                                        |
            4 -> 7          : 0        |                                        |
            8 -> 15         : 1        |***                                     |
           16 -> 31         : 2        |*******                                 |
           32 -> 63         : 11       |****************************************|
           64 -> 127        : 8        |*****************************           |
          128 -> 255        : 3        |**********                              |
    

    完整代码在这里: https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/runqlat

  4. 添加对 uprobe, tc 等多种类型 map 的支持, 允许用标记实现添加额外 attach 信息, 例如:

    
    /// @tchook {"ifindex":1, "attach_point":"BPF_TC_INGRESS"}
    /// @tcopts {"handle":1,  "priority":1}
    SEC("tc")
    int tc_ingress(struct __sk_buff *ctx)
    {
        void *data_end = (void *)(__u64)ctx->data_end;
        void *data = (void *)(__u64)ctx->data;
        struct ethhdr *l2;
        struct iphdr *l3;
    
        if (ctx->protocol != bpf_htons(ETH_P_IP))
            return TC_ACT_OK;
    
        l2 = data;
        if ((void *)(l2 + 1) > data_end)
            return TC_ACT_OK;
    
        l3 = (struct iphdr *)(l2 + 1);
        if ((void *)(l3 + 1) > data_end)
            return TC_ACT_OK;
    
        bpf_printk("Got IP packet: tot_len: %d, ttl: %d", bpf_ntohs(l3->tot_len), l3->ttl);
        return TC_ACT_OK;
    }
    

编译方面:编译体验优化、格式改进

  1. 完全重构了编译工具链和配置文件格式,回归本质的配置文件 + ebpf 字节码 .o 的形式,不强制打包成 JSON 格式,对分发使用和人类编辑配置文件更友好,同时也可以更好地和 libbpf 相关工具链兼容;
  2. 支持 JSON 和 YAML 两种形式的配置文件(xxx.skel.yaml 和 xxx.skel.json),或打包成 package.json 和 package.yaml 进行分发;
  3. 尽可能使用 BTF 信息表达符号类型,并且把 BTF 信息隐藏在二进制文件中,让配置文件更可读和可编辑,同时复用 libbpf 提供的 BTF 处理机制,完善对于类型的处理;
  4. 支持更多的数据导出类型:enum、struct、bool 等等
  5. 编译部分可以不依赖于 docker 运行,可以安装二进制和头文件到 ~/.eunomia(对嵌入式或者国内网络更友好,更方便使用),原本 docker 的使用方式还是可以继续使用;
  6. 文件名没有特定限制,不需要一定是 xxx.bpf.h 和 xxx.bpf.c,可以通过 ecc 指定当前目录下需要编译的文件;
  7. 把 example 中旧的 xxx.bpf.h 头文件修改为 xxx.h,和 libbpf-tools 和 libbpf-boostrap 保持一致,确保 0 代码修改即可复用 libbpf 相关代码生态;
  8. 大幅度优化编译速度和减少编译依赖,使用 Rust 重构了编译工具链,替换原先的 python 脚本;

在配置文件中, 可以直接修改 progs/attach 控制挂载点,variables/value 控制全局变量,maps/data 控制在加载 ebpf 程序时往 map 里面放什么数据,export_types/members 控制往用户态传输什么数据格式,而不需要重新编译 eBPF 程序。配置文件和 bpf.o 二进制是配套的,应该搭配使用,或者打包成一个 package.json/yaml 分发。打包的时候会进行压缩,一般来说压缩后的配置文件和二进制合起来的大小在数十 kb 。

配置文件举例:

bpf_skel:
  data_sections:
  - name: .rodata
    variables:
    - name: min_duration_ns
      type: unsigned long long
      value: 100
  maps:
  - ident: exec_start
    name: exec_start
    data:
      - key: 123
        value: 456
  - ident: rb
    name: rb
  - ident: rodata
    mmaped: true
    name: client_b.rodata
  obj_name: client_bpf
  progs:
  - attach: tp/sched/sched_process_exec
    link: true
    name: handle_exec
export_types:
- members:
  - name: pid
    type: int
  - name: ppid
    type: int
  - name: comm
    type: char[16]
  - name: filename
    type: char[127]
  - name: exit_event
    type: bool
  name: event
  type_id: 613

下载安装 eunomia-bpf

  • Install the ecli tool for running eBPF program from the cloud:

    $ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
    $ ./ecli -h
    Usage: ecli [--help] [--version] [--json] [--no-cache] url-and-args
    ....
    
  • Install the compiler-toolchain for compiling eBPF kernel code to a config file or WASM module:

    $ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/eunomia.tar.gz
    $ tar -xvf eunomia.tar.gz -C ~
    $ export PATH=$PATH:~/.eunomia/bin
    $ ecc -h
    eunomia-bpf compiler
    Usage: ecc [OPTIONS] <SOURCE_PATH> [EXPORT_EVENT_HEADER]
    ....
    

    or use the docker image for compile:

    docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest # compile with docker. `pwd` should contains *.bpf.c files and *.h files.
    

下一步发展的计划

  1. 和更多的社区伙伴合作, 并逐步形成标准化的, 使用配置文件或 WASM 二进制进行打包分发, 一次编译, 到处运行的 eBPF 程序格式;
  2. 和 LMP 社区一起, 完善基于 ORAS, OCI 和 WASM 的 eBPF 程序分发和运行时标准, 让任意 eBPF 应用均可从云端一行命令拉下来直接运行, 或轻松嵌入其他应用中使用, 无需关注架构, 内核版本等细节;
  3. 尝试和 Coolbpf 社区一同完善远程编译, 低版本支持的特性, 以及支持 RPC 的 libbpf 库;
  4. 完善用户态 WASM 和 eBPF 程序之间的互操作性, 探索 WASI 的相关扩展;

参考资料

  1. 当 WASM 遇见 eBPF :使用 WebAssembly 编写、分发、加载运行 eBPF 程序
  2. 如何在 Linux 显微镜(LMP)项目中开启 eBPF 之旅?
  3. 龙蜥社区 eunomia-bpf 项目主页
  4. eunomia-bpf 项目文档
  5. LMP 项目

我们的微信群

eBPF 入门开发实践指南一:介绍与快速上手

1. 什么是eBPF

Linux内核一直是实现监控/可观测性、网络和安全功能的理想地方, 但是直接在内核中进行监控并不是一个容易的事情。在传统的Linux软件开发中, 实现这些功能往往都离不开修改内核源码或加载内核模块。修改内核源码是一件非常危险的行为, 稍有不慎可能便会导致系统崩溃,并且每次检验修改的代码都需要重新编译内核,耗时耗力。

加载内核模块虽然来说更为灵活,不需要重新编译源码,但是也可能导致内核崩溃,且随着内核版本的变化 模块也需要进行相应的修改,否则将无法使用。

在这一背景下,eBPF技术应运而生。它是一项革命性技术,能在内核中运行沙箱程序(sandbox programs),而无需修改内核源码或者加载内核模块。用户可以使用其提供的各种接口,实现在内核中追踪、监测系统的作用。

1.1. 起源

eBPF的雏形是BPF(Berkeley Packet Filter, 伯克利包过滤器)。BPF于 1992年被Steven McCanne和Van Jacobson在其论文 提出。二人提出BPF的初衷是是提供一种新的数据包过滤方法,该方法的模型如下图所示。

相较于其他过滤方法,BPF有两大创新点,首先是它使用了一个新的虚拟机,可以有效地工作在基于寄存器结构的CPU之上。其次是其不会全盘复制数据包的所有信息,只会复制相关数据,可以有效地提高效率。这两大创新使得BPF在实际应用中得到了巨大的成功,在被移植到Linux系统后,其被上层的libcaptcpdump等应用使用,是一个性能卓越的工具。

传统的BPF是32位架构,其指令集编码格式为:

  • 16 bit: 操作指令
  • 8 bit: 下一条指令跳向正确目标的偏移量
  • 8 bit: 下一条指令跳往错误目标的偏移量

经过十余年的沉积后,2013年,Alexei Starovoitov对BPF进行了彻底地改造,改造后的BPF被命名为eBPF(extended BPF),于Linux Kernel 3.15中引入Linux内核源码。 eBPF相较于BPF有了革命性的变化。首先在于eBPF支持了更多领域的应用,它不仅支持网络包的过滤,还可以通过 kprobetracepoint,lsm等Linux现有的工具对响应事件进行追踪。另一方面,其在使用上也更为 灵活,更为方便。同时,其JIT编译器也得到了升级,解释器也被替换,这直接使得其具有达到平台原生的 执行性能的能力。

1.2. 执行逻辑

eBPF在执行逻辑上和BPF有相似之处,eBPF也可以认为是一个基于寄存器的,使用自定义的64位RISC指令集的 微型"虚拟机"。它可以在Linux内核中,以一种安全可控的方式运行本机编译的eBPF程序并且访问内核函数和内存的子集。

在写好程序后,我们将代码使用llvm编译得到使用BPF指令集的ELF文件,解析出需要注入的部分后调用函数将其 注入内核。用户态的程序和注入内核态中的字节码公用一个位于内核的eBPF Map进行通信,实现数据的传递。同时, 为了防止我们写入的程序本身不会对内核产生较大影响,编译好的字节码在注入内核之前会被eBPF校验器严格地检查。

eBPF程序是由事件驱动的,我们在程序中需要提前确定程序的执行点。编译好的程序被注入内核后,如果提前确定的执行点 被调用,那么注入的程序就会被触发,按照既定方式处理。

1.3. 架构

1.3.1. 寄存器设计

eBPF有11个寄存器,分别是R0~R10,每个寄存器均是64位大小,有相应的32位子寄存器,其指令集是固定的64位宽。

1.3.2. 指令编码格式

eBPF指令编码格式为:

  • 8 bit: 存放真实指令码
  • 4 bit: 存放指令用到的目标寄存器号
  • 4 bit: 存放指令用到的源寄存器号
  • 16 bit: 存放偏移量,具体作用取决于指令类型
  • 32 bit: 存放立即数

1.4. 本节参考文章

A thorough introduction to eBPF bpf简介 bpf架构知识

2. 如何使用eBPF编程

原始的eBPF程序编写是非常繁琐和困难的。为了改变这一现状, llvm于2015年推出了可以将由高级语言编写的代码编译为eBPF字节码的功能,同时,其将bpf() 等原始的系统调用进行了初步地封装,给出了libbpf库。这些库会包含将字节码加载到内核中 的函数以及一些其他的关键函数。在Linux的源码包的samples/bpf/目录下,有大量Linux 提供的基于libbpf的eBPF样例代码。

一个典型的基于libbpf的eBPF程序具有*_kern.c*_user.c两个文件, *_kern.c中书写在内核中的挂载点以及处理函数,*_user.c中书写用户态代码, 完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考该视频 然而由于该方法仍然较难理解且入门存在一定的难度,因此现阶段的eBPF程序开发大多基于一些工具,比如:

  • BCC
  • BPFtrace
  • libbpf-bootstrap

以及还有比较新的工具,例如 eunomia-bpf 将 CO-RE eBPF 功能作为服务运行,包含一个工具链和一个运行时,主要功能包括:

  • 不需要再为每个 eBPF 工具编写用户态代码框架:大多数情况下只需要编写内核态应用程序,即可实现正确加载运行 eBPF 程序;同时所需编写的内核态代码和 libbpf 完全兼容,可轻松实现迁移;
  • 提供基于 async Rust 的 Prometheus 或 OpenTelemetry 自定义可观测性数据收集器,通常仅占用不到1%的资源开销,编写内核态代码和 yaml 配置文件即可实现 eBPF 信息可视化,编译后可在其他机器上通过 API 请求直接部署;

2.1. BCC

BCC全称为BPF Compiler Collection,该项目是一个python库, 包含了完整的编写、编译、和加载BPF程序的工具链,以及用于调试和诊断性能问题的工具。

自2015年发布以来,BCC经过上百位贡献者地不断完善后,目前已经包含了大量随时可用的跟踪工具。其官方项目库 提供了一个方便上手的教程,用户可以快速地根据教程完成BCC入门工作。

用户可以在BCC上使用Python、Lua等高级语言进行编程。 相较于使用C语言直接编程,这些高级语言具有极大的便捷性,用户只需要使用C来设计内核中的 BPF程序,其余包括编译、解析、加载等工作在内,均可由BCC完成。

然而使用BCC存在一个缺点便是在于其兼容性并不好。基于BCC的 eBPF程序每次执行时候都需要进行编译,编译则需要用户配置相关的头文件和对应实现。在实际应用中, 相信大家也会有体会,编译依赖问题是一个很棘手的问题。也正是因此,在本项目的开发中我们放弃了BCC, 选择了可以做到一次编译-多次运行的libbpf-bootstrap工具。

2.2. libbpf-bootstrap

libbpf-bootstrap是一个基于libbpf库的BPF开发脚手架,从其 github 上可以得到其源码。

libbpf-bootstrap综合了BPF社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实 现了一次编译,重复使用的目的。

基于libbpf-bootstrap的BPF程序对于源文件有一定的命名规则, 用于生成内核态字节码的bpf文件以.bpf.c结尾,用户态加载字节码的文件以.c结尾,且这两个文件的 前缀必须相同。

基于libbpf-bootstrap的BPF程序在编译时会先将*.bpf.c文件编译为 对应的.o文件,然后根据此文件生成skeleton文件,即*.skel.h,这个文件会包含内核态中定义的一些 数据结构,以及用于装载内核态代码的关键函数。在用户态代码include此文件之后调用对应的装载函数即可将 字节码装载到内核中。同样的,libbpf-bootstrap也有非常完备的入门教程,用户可以在该处 得到详细的入门操作介绍。

2.3 eunomia-bpf

开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang等编译环境,每次运行的时候执行本地或远程编译过程,资源消耗较大;使用原生的 CO-RE libbpf时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案

eunomia-bpf 以一次编译、到处运行的libbpf 为基础实现,是一套编译工具链和运行时,以及一些附加项目,我们希望做到让 eBPF 程序:

  • 真正像 JavaScript 或者 WASM 那样易于分发和运行,或者说内核态或可观测性层面的 FaaS:eBPF 即服务;
  • 让 eBPF 程序的编译和运行过程大大简化,抛去繁琐的用户态模板编写、繁琐的 BCC 安装流程,只需要编写内核态 eBPF 程序,编译后即可在不同机器上任意内核版本下运行,并且轻松获取可视化结果。

eunomia-bpf 具备的优势

  • 大多数情况下只需要编写内核态应用程序,不需要编写任何用户态辅助框架代码;
  • 和 libbpf 的原生 C 代码几乎完全兼容,迁移成本极小;
  • 仅需一个数十kb的请求,包含预编译 eBPF 程序字节码和辅助信息,即可实现多种完全不同功能的 eBPF 程序的热插拔、热更新;加载时间通常为数十毫秒且内存占用少;(例如使用 libbpf-bootstrap/bootstrap.bpf.c ,热加载时间约为 50ms,运行时内存占用约为 5 MB,同时新增更多的 eBPF 程序通常只会增加数百 kB 的内存占用)
  • 相比于传统的基于 BCC 或远程编译的分发方式,分发时间和内存占用均减少了一到二个数量级;
  • 运行时只需数 MB 且无 llvm、clang 依赖,即可实现一次编译、到处运行;将 eBPF 程序的编译和运行完全解耦,本地预编译好的 eBPF 程序可以直接发送到不同内核版本的远端执行;
  • 支持动态分发和加载 tracepoints, fentry, kprobe, lsm 等类型的大多数 eBPF 程序,也支持 ring buffer、perf event 等方式向用户态空间传递信息;
  • 提供基于 async Rust 的自定义 Prometheus 或 OpenTelemetry 可观测性数据收集器,通常仅占用不到1%的资源开销;
  • 提供 C、Rust 等语言的 SDK,可轻松集成到其他项目中;

eunomia-bpf 的 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf

2.4 快速上手开发

创建一个新项目:

mkdir hello
cd hello

在 hello 文件夹中创建一个新的 hello.bpf.c,内容如下:

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

typedef int pid_t;

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

SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
 pid_t pid = bpf_get_current_pid_tgid() >> 32;
 bpf_printk("BPF triggered from PID %d.\n", pid);
 return 0;
}

假设hello文件夹的父目录是/path/to/repo,接下来的步骤:

# 下载安装 ecli 二进制
wget https://aka.pw/bpf-ecli -O ./ecli && chmod +x ./ecli
# 使用容器进行编译,生成一个 package.json 文件,里面是已经编译好的代码和一些辅助信息
docker run -it -v /path/to/repo/hello:/src yunwei37/ebpm:latest
# 运行 eBPF 程序(root shell)
sudo ./ecli run package.json  

使用 docker 的时候需要把包含 .bpf.c 文件的目录挂载到容器的 /src 目录下,目录中只有一个 .bpf.c 文件;

它会追踪所有进行 write 系统调用的进程的 pid:

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

cat-42755   [003] d...1 48755.529860: bpf_trace_printk: BPF triggered from PID 42755.
             cat-42755   [003] d...1 48755.529874: bpf_trace_printk: BPF triggered from PID 42755.

其中 SEC("tp/syscalls/sys_enter_write") 告诉了我们需要追踪的系统调用,bpf_printk 是 eBPF 程序中的打印函数,bpf_get_current_pid_tgid 是获取当前进程的 pid 的函数。

我们编译好的 eBPF 代码同样可以适配多种内核版本,可以直接把 package.json 复制到另外一个机器上,然后不需要重新编译就可以直接运行(CO-RE:Compile Once Run Every Where);也可以通过网络传输和分发 package.json,通常情况下,压缩后的版本只有几 kb 到几十 kb。

更进一步的文档可以参考:https://eunomia-bpf.github.io/

eBPF 入门实践教程:编写 eBPF 程序监控打开文件路径并使用 Prometheus 可视化

背景

通过对 open 系统调用的监测,opensnoop可以展现系统内所有调用了 open 系统调用的进程信息。

使用 ecli 一键运行

$ # 下载安装 ecli 二进制
$ wget https://aka.pw/bpf-ecli -O ./ecli && chmod +x ./ecli
$ # 使用 url 一键运行
$ ./ecli run https://eunomia-bpf.github.io/eunomia-bpf/opensnoop/package.json

running and waiting for the ebpf events from perf event...
time ts pid uid ret flags comm fname
00:58:08 0 812 0 9 524288 vmtoolsd /etc/mtab
00:58:08 0 812 0 11 0 vmtoolsd /proc/devices
00:58:08 0 34351 0 24 524288 ecli /etc/localtime
00:58:08 0 812 0 9 0 vmtoolsd /sys/class/block/sda5/../device/../../../class
00:58:08 0 812 0 -2 0 vmtoolsd /sys/class/block/sda5/../device/../../../label
00:58:08 0 812 0 9 0 vmtoolsd /sys/class/block/sda1/../device/../../../class
00:58:08 0 812 0 -2 0 vmtoolsd /sys/class/block/sda1/../device/../../../label
00:58:08 0 812 0 9 0 vmtoolsd /run/systemd/resolve/resolv.conf
00:58:08 0 812 0 9 0 vmtoolsd /proc/net/route
00:58:08 0 812 0 9 0 vmtoolsd /proc/net/ipv6_route

实现

使用 eunomia-bpf 可以帮助你只需要编写内核态应用程序,不需要编写任何用户态辅助框架代码;需要编写的代码由两个部分组成:

  • 头文件 opensnoop.h 里面定义需要导出的 C 语言结构体:
  • 源文件 opensnoop.bpf.c 里面定义 BPF 代码:

头文件 opensnoop.h

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __OPENSNOOP_H
#define __OPENSNOOP_H

#define TASK_COMM_LEN 16
#define NAME_MAX 255
#define INVALID_UID ((uid_t)-1)

// used for export event
struct event {
	/* user terminology for pid: */
	unsigned long long ts;
	int pid;
	int uid;
	int ret;
	int flags;
	char comm[TASK_COMM_LEN];
	char fname[NAME_MAX];
};

#endif /* __OPENSNOOP_H */

opensnoop 的实现逻辑比较简单,它在 sys_enter_opensys_enter_openat 这两个追踪点下 加了执行函数,当有 open 系统调用发生时,执行函数便会被触发。同样在,在对应的 sys_exit_opensys_exit_openat 系统调用下,opensnoop 也加了执行函数。

源文件 opensnoop.bpf.c

// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2019 Facebook
// Copyright (c) 2020 Netflix
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include "opensnoop.h"

struct args_t {
	const char *fname;
	int flags;
};

const volatile pid_t targ_pid = 0;
const volatile pid_t targ_tgid = 0;
const volatile uid_t targ_uid = 0;
const volatile bool targ_failed = false;

struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 10240);
	__type(key, u32);
	__type(value, struct args_t);
} start SEC(".maps");

struct {
	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
	__uint(key_size, sizeof(u32));
	__uint(value_size, sizeof(u32));
} events SEC(".maps");

static __always_inline bool valid_uid(uid_t uid) {
	return uid != INVALID_UID;
}

static __always_inline
bool trace_allowed(u32 tgid, u32 pid)
{
	u32 uid;

	/* filters */
	if (targ_tgid && targ_tgid != tgid)
		return false;
	if (targ_pid && targ_pid != pid)
		return false;
	if (valid_uid(targ_uid)) {
		uid = (u32)bpf_get_current_uid_gid();
		if (targ_uid != uid) {
			return false;
		}
	}
	return true;
}

SEC("tracepoint/syscalls/sys_enter_open")
int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx)
{
	u64 id = bpf_get_current_pid_tgid();
	/* use kernel terminology here for tgid/pid: */
	u32 tgid = id >> 32;
	u32 pid = id;

	/* store arg info for later lookup */
	if (trace_allowed(tgid, pid)) {
		struct args_t args = {};
		args.fname = (const char *)ctx->args[0];
		args.flags = (int)ctx->args[1];
		bpf_map_update_elem(&start, &pid, &args, 0);
	}
	return 0;
}

SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
{
	u64 id = bpf_get_current_pid_tgid();
	/* use kernel terminology here for tgid/pid: */
	u32 tgid = id >> 32;
	u32 pid = id;

	/* store arg info for later lookup */
	if (trace_allowed(tgid, pid)) {
		struct args_t args = {};
		args.fname = (const char *)ctx->args[1];
		args.flags = (int)ctx->args[2];
		bpf_map_update_elem(&start, &pid, &args, 0);
	}
	return 0;
}

static __always_inline
int trace_exit(struct trace_event_raw_sys_exit* ctx)
{
	struct event event = {};
	struct args_t *ap;
	int ret;
	u32 pid = bpf_get_current_pid_tgid();

	ap = bpf_map_lookup_elem(&start, &pid);
	if (!ap)
		return 0;	/* missed entry */
	ret = ctx->ret;
	if (targ_failed && ret >= 0)
		goto cleanup;	/* want failed only */

	/* event data */
	event.pid = bpf_get_current_pid_tgid() >> 32;
	event.uid = bpf_get_current_uid_gid();
	bpf_get_current_comm(&event.comm, sizeof(event.comm));
	bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname);
	event.flags = ap->flags;
	event.ret = ret;

	/* emit event */
	bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
			      &event, sizeof(event));

cleanup:
	bpf_map_delete_elem(&start, &pid);
	return 0;
}

SEC("tracepoint/syscalls/sys_exit_open")
int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx)
{
	return trace_exit(ctx);
}

SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx)
{
	return trace_exit(ctx);
}

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

在 enter 环节,opensnoop 会记录调用者的 pid, comm 等基本信息,并存入 map 中。在 exit 环节,opensnoop 会根据 pid 读出之前存入的数据,再结合捕获的其他数据,输出到用户态处理函数中,展现给用户。

完整示例代码请参考:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/opensnoop

把头文件和源文件放在独立的目录里面,编译运行:

$ # 使用容器进行编译,生成一个 package.json 文件,里面是已经编译好的代码和一些辅助信息
$ docker run -it -v /path/to/opensnoop:/src yunwei37/ebpm:latest
$ # 运行 eBPF 程序(root shell)
$ sudo ecli run package.json

Prometheus 可视化

编写 yaml 配置文件:

programs:
  - name: opensnoop
    metrics:
      counters:
        - name: eunomia_file_open_counter
          description: test
          labels:
            - name: pid
            - name: comm
            - name: filename
              from: fname
    compiled_ebpf_filename: package.json

使用 eunomia-exporter 实现导出信息到 Prometheus:

  • 通过 https://github.com/eunomia-bpf/eunomia-bpf/releases 下载 eunomia-exporter
$ ls
config.yaml  eunomia-exporter package.json
$ sudo ./eunomia-exporter

Running ebpf program opensnoop takes 46 ms
Listening on http://127.0.0.1:8526
running and waiting for the ebpf events from perf event...
Receiving request at path /metrics

result

总结和参考资料

opensnoop 通过对 open 系统调用的追踪,使得用户可以较为方便地掌握目前系统中调用了 open 系统调用的进程信息。

参考资料:

  • 源代码:https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/opensnoop
  • libbpf 参考代码:https://github.com/iovisor/bcc/blob/master/libbpf-tools/opensnoop.bpf.c
  • eunomia-bpf 手册:https://eunomia-bpf.github.io/