bootstrap
is an example of a simple (but realistic) BPF application. It
tracks process starts (exec()
family of syscalls, to be precise) and exits
and emits data about filename, PID and parent PID, as well as exit status and
duration of the process life. With -d <min-duration-ms>
you can specify
minimum duration of the process to log. In such mode process start
(technically, exec()
) events are not output (see example output below).
bootstrap
was created in the similar spirit as
libbpf-tools from
BCC package, but is designed to be more stand-alone and with simpler Makefile
to simplify adoption to user's particular needs. It demonstrates the use of
typical BPF features:
- cooperating BPF programs (tracepoint handlers for process
exec
andexit
events, in this particular case); - BPF map for maintaining the state;
- BPF ring buffer for sending data to user-space;
- global variables for application behavior parameterization.
- it utilizes BPF CO-RE and vmlinux.h to read extra process information from
kernel's
struct task_struct
.
bootstrap
is intended to be the starting point for your own BPF application,
with things like BPF CO-RE and vmlinux.h, consuming BPF ring buffer data,
command line arguments parsing, graceful Ctrl-C handling, etc. all taken care
of for you, which are crucial but mundane tasks that are no fun, but necessary
to be able to do anything useful. Just copy/paste and do simple renaming to get
yourself started.
Here's an example output:
$ sudo sudo ./wasm-bpf bootstrap.wasm -h
BPF bootstrap demo application.
It traces process start and exits and shows associated
information (filename, process duration, PID and PPID, etc).
USAGE: ./bootstrap [-d <min-duration-ms>] -v
$ sudo ./wasm-bpf bootstrap.wasm
TIME EVENT COMM PID PPID FILENAME/EXIT CODE
18:57:58 EXEC sed 74911 74910 /usr/bin/sed
18:57:58 EXIT sed 74911 74910 [0] (2ms)
18:57:58 EXIT cat 74912 74910 [0] (0ms)
18:57:58 EXEC cat 74913 74910 /usr/bin/cat
18:57:59 EXIT cat 74913 74910 [0] (0ms)
18:57:59 EXEC cat 74914 74910 /usr/bin/cat
18:57:59 EXIT cat 74914 74910 [0] (0ms)
18:57:59 EXEC cat 74915 74910 /usr/bin/cat
18:57:59 EXIT cat 74915 74910 [0] (1ms)
18:57:59 EXEC sleep 74916 74910 /usr/bin/sleep
The original c code is from libbpf-bootstrap.
We can provide a similar developing experience as the libbpf-bootstrap development. Just run make
to build the wasm binary:
make
This would invoke the following steps:
-
build the BPF program using
clang
andllvm-strip
to strip debug info:clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I../../third_party/vmlinux/x86/ -idirafter /usr/lib/llvm-15/lib/clang/15.0.2/include -idirafter /usr/local/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include -c bootstrap.bpf.c -o bootstrap.bpf.o
The kernel part of the BPF program is exactly the same as the libbpf(Or any other style can be compiled by clang. The BCC style can be compiled in this way once the bcc to libbpf converter is completed.
-
generate the C header file from the BPF program:
../../third_party/bpftool/src/bpftool gen skeleton -j bootstrap.bpf.o > bootstrap.skel.h
The C skel would include a skeleton for the BPF program to operate the BPF object, and control the life cycle of the BPF program, for example:
struct bootstrap_bpf { struct bpf_object_skeleton *skeleton; struct bpf_object *obj; struct { struct bpf_map *exec_start; struct bpf_map *rb; struct bpf_map *rodata; } maps; struct { struct bpf_program *handle_exec; struct bpf_program *handle_exit; } progs; struct bootstrap_bpf__rodata { unsigned long long min_duration_ns; } *rodata; struct bootstrap_bpf__bss { uint64_t /* pointer */ name_ptr; } *bss; };
Because the struct layout of the host (or eBPF side) may be different from the struct layout of the target (Wasm side), all pointer will be converted to integers base on the host's pointer size. For example, the
name_ptr
is a pointer to thename
field in thestruct exec_start_t
struct. Besides, the padding bytes will be explicitly added to the struct to make sure the struct layout is the same as the target side, for example, usingchar __pad0[4];
. This is generated by our modifiedbpftool
tool for Wasm. -
build user space wasm code
/opt/wasi-sdk/bin/clang -O2 --sysroot=/opt/wasi-sdk/share/wasi-sysroot -Wl,--allow-undefined -o bootstrap.wasm bootstrap.c
A wasi-sdk is required to build the wasm binary. You can use the emcc toolchain to build the wasm binary as well, the command should be similar.
You can run the following command to install the wasi-sdk:
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-17/wasi-sdk-17.0-linux.tar.gz tar -zxf wasi-sdk-17.0-linux.tar.gz sudo mkdir -p /opt/wasi-sdk/ && sudo mv wasi-sdk-17.0/* /opt/wasi-sdk/
Since the struct layout of the host (or eBPF side) may be different from the struct layout of the target (Wasm side), you need to take care of the struct layout shared between the user space code. You can use the ecc and our wasm-bpftool for generating the C header file for the user space code:
ecc bootstrap.h --header-only ../../third_party/bpftool/src/bpftool btf dump file bootstrap.bpf.o format c -j > bootstrap.wasm.h
The
ecc
compiler in eunomia-bpf will use libclang and llvm to find all struct definitions in the header file, and automatically add more btf info to the ebpf object. The originalclang
may not always generate enough btf info for the wasm-bpf tool to generate the correct C header file.Note: This process and tools is not always required, you can do that mannually. You can mannually write all event structs definitions with
__attribute__((packed))
to avoid padding bytes, and convert all pointer to correct integers between the host and the wasm side. All types must be defined the same size and layout as the host side in wasm as well. This would be easy for simple events, but it would be hard for complex programs, so we create the wasm specificbpftool
to generate the C header file for the user space code contains all type defines and correct struct layout from theBTF
info.We have create a specical POC tool outside of
bpftool
for generate C structs serialization-free bindings between eBPF/host side and Wasm, you can find it in c-struct-bindgen. More details about how to deal with the struct layout issue can be found in the README of the c-struct-bindgen tool.The libbpf API for wasm program is provided as an header only library, you can find it in
libbpf-wasm.h
. The wasm program can use the libbpf API and syscall to operate the BPF object, for example:/* Load and verify BPF application */ skel = bootstrap_bpf__open(); /* Parameterize BPF code with minimum duration parameter */ skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL; /* Load & verify BPF programs */ err = bootstrap_bpf__load(skel); /* Attach tracepoints */ err = bootstrap_bpf__attach(skel);
The
rodata
section is used to store the global variables in the BPF program, and thebss
section is used to store the global variables in the user space code, which will be memory mapped to the correct offset at thebpftool gen skeleton
time, so libelf library is not required to be compiled in Wasm and the runtime can still dynamically load and operate the BPF object.The wasm side C code will be a slightly different from the native libbpf code, but it would provide the most ability from the eBPF side, for example, polling from the ring buffer or perf buffer, accessing the map from both the Wasm side and eBPF side, loading, attaching and detaching BPF programs, etc. It can support a lare number of eBPF program types and maps, covering the use cases of most eBPF programs from tracing, networking, security, etc.
Because some feature is missing in wasm side, for example, the signal handler is not support yet(2023/2), the original C code cannot be compiled to wasm directly, you need to modify the code slightly to make it work. We would try our best to make the wasm side libbpf API as close as possible to the native libbpf API, so maybe the user space code can be compiled to wasm directly in the future. More language bindings(Rust, Go, etc...) for wasm side bpf API will also be provided soon.
The polling API would be a wrapped for both ring buffer and perf buffer, and the user space code can use the same API to poll events from either ring buffer or perf buffer, depends on the type specified in the BPF program. For example, a ring buffer polling for a map defined as
BPF_MAP_TYPE_RINGBUF
:struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } rb SEC(".maps");
You can use the following code to poll events from the ring buffer:
rb = bpf_buffer__open(skel->maps.rb, handle_event, NULL); /* Process events */ printf("%-8s %-5s %-16s %-7s %-7s %s\n", "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE"); while (!exiting) { // poll buffer err = bpf_buffer__poll(rb, 100 /* timeout, ms */);
No serialization overhead is required for the ring buffer polling. The
bpf_buffer__poll
API will call thehandle_event
function to process the event data in the ring buffer.The runtime is build on the top of libbpf CO-RE(Compile Once – Run Everywhere) API to load bpf object into the kernel, so a wasm-bpf program will not rely on the kernel version where it's built, and can be run on any kernel version with BPF CO-RE support.
The size of
bootstrap.wasm
would be only~90K
. It would be very easy to distributed over the network and can dynamically deploy, load and run on another machine in less than100ms
. Nokernel header
,LLVM
,clang
dependency is required at the runtime, and don't need to do the heavy compilation work!For a more complex example, you can find the runqlat program in the
examples
directory.