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).

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.