12. eBPF
To deepen our understanding of eBPF we will write and compile a small eBPF app:
Task 12.1: Hello World
ebpf-go is a pure Go library that provides utilities for loading, compiling, and debugging eBPF programs written by the cilium project.
We will use this library and add our own hello world app as an example to it:
git clone https://github.com/cilium/ebpf.git
cd ebpf/
git checkout v0.9.3
cd examples
mkdir helloworld
cd helloworld
In the helloworld directory create two files named helloworld.bpf.c (eBPF code) and helloworld.go (loading, user side):
helloworld.bpf.c:
#include "common.h"
// SEC is a macro that expands to create an ELF section which bpf loaders parse.
// we want our function to be executed whenever syscall execve (program execution) is called
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
char msg[] = "Hello world";
// bpf_printk is a bpf helper function which writes strings to /sys/kernel/debug/tracing/trace_pipe (good for debugging purposes)
bpf_printk("%s", msg);
// bpf programs need to return an int
return 0;
}
char LICENSE[] SEC("license") = "GPL";helloworld.go:
package main
import (
"log"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
func main() {
// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// Here we load our bpf code into the kernel, these functions are in the
// .go file created by bpf2go
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %s", err)
}
defer objs.Close()
//SEC("tracepoint/syscalls/sys_enter_execve")
kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
if err != nil {
log.Fatalf("opening tracepoint: %s", err)
}
defer kp.Close()
for {
}
log.Println("Received signal, exiting program..")
}
To compile the C code into ebpf bytecode with the corresponding Go source files we use a tool named bpf2go along with clang. For a stable outcome we use the toolchain inside a docker container:
docker pull "ghcr.io/cilium/ebpf-builder:1666886595"
docker run -it --rm -v "$(pwd)/../..":/ebpf \
-w /ebpf/examples/helloworld \
--env MAKEFLAGS \
--env CFLAGS="-fdebug-prefix-map=/ebpf=." \
--env HOME="/tmp" \
"ghcr.io/cilium/ebpf-builder:1666886595" /bin/bash
Now in the container we generate the ELF and go files:
GOPACKAGE=main go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 -cflags '-O2 -g -Wall -Werror' bpf helloworld.bpf.c -- -I../headers
Let us examine the newly created files: bpf_bpfel.go/bpf_bpfeb.go contain the go code for the user state side of our app.
The bpf_bpfel.o/bpf_bpfeb.o files are ELF files and can be examined using readelf:
readelf --section-details --headers bpf_bpfel.o
We see two things:
- that Machine reads “Linux BPF” and
- our tracepoint sys_enter_execve in the sections part (tracepoint/syscalls/sys_enter_execve).
Note
There are always two files created: bpf_bpfel.o for little endian systems (like x86) and bpfen.o for big endian systems.Now we have everything in place to build our app:
go mod tidy
go build helloworld.go bpf_bpfel.go
exit #exit container
Let us cat tracepipe first in a second terminal (webshell: don’t forget to connect to the vm first):
sudo cat /sys/kernel/debug/tracing/trace_pipe
and in the first terminal execute our eBPF app:
sudo ./helloworld
Now we can see, that for each programm called in linux, our code is executed and writes “Hello world” to trace_pipe.
Close now apps by hitting Ctrl+c, you can also close the second terminal.