Build Your First eBPF Hello World: From Compilation to Tracepoint Debugging
This tutorial walks through installing the required toolchain, writing a minimal eBPF program, compiling it with eunomia‑bpf or cilium/ebpf, loading it from user space, attaching it to a tracepoint, and observing kernel‑level output, while explaining each step and the underlying concepts.
Introduction
Every story starts with a "Hello World" program, and eBPF is no exception. This article shows how to write a simple eBPF program that traces a system call, covering the full development workflow from environment setup to runtime debugging.
Problem Statement
The goal is to create the simplest possible eBPF Hello World program and understand how the kernel‑side logic and the user‑side loader interact.
Required Environment
CPU: Intel(R) Core(TM) i3‑2310M @ 2.10GHz (4 logical cores)
Memory: 8 GB
OS: Ubuntu 24.04.1 LTS (Noble Numbat)
Linux kernel: 6.8.0‑44‑generic
LLVM/Clang: 18.1.3
GCC: 13.2.0
eunomia‑bpf (ecc): 0.3.4
cilium/ebpf: v0.16.0
eBPF Program Structure
An eBPF program consists of two parts:
Kernel‑mode code – the actual BPF logic that runs in the kernel.
User‑mode code – a loader that compiles, loads, and monitors the kernel program. The ip command can also be used for loading/unloading.
Solution Overview
After choosing a development framework (BCC, libbpf, cilium/ebpf, or eunomia‑bpf), you can start writing both the kernel and user components. The following sections demonstrate two concrete methods.
Method 1: Using eunomia‑bpf
Step 1 – Download the ecli runner:
$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
$ ./ecli -hStep 2 – Download the compiler toolchain ( ecc ) to turn C source into a BPF object or WASM module:
$ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc
$ ./ecc -h
Usage: ecc [OPTIONS] <SOURCE_PATH> [EXPORT_EVENT_HEADER]Step 3 – (Optional) Compile inside Docker:
$ docker run -it -v `pwd`:/src ghcr.io/eunomia-bpf/ecc-$(uname -m):latest \
# The current directory must contain *.bpf.c and *.h files
export PATH=PATH:~/.eunomia/bin
Compiling bpf object...
Packing ebpf object and config into /src/package.json...Using eunomia‑bpf automates compilation and embeds metadata, so the user‑space loader can run the program without writing any additional code.
Compilation is fully automated, reducing build‑time errors.
Running is "click‑free" – the generated metadata is sufficient for execution.
Method 2: Using cilium/ebpf
1. Install dependencies
LLVM and Clang – needed to compile the BPF program.
Go – for writing the user‑space loader. bpf2go – generates Go bindings from the compiled BPF object.
2. Write the kernel‑mode C program ( minimal.bpf.c )
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx)
{
char msg[] = "Hello, eBPF!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";This program attaches to the sys_enter_execve tracepoint and prints a static string via bpf_trace_printk.
3. Generate Go bindings
//go:generate bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf hello.bpf.c
package main
import (
"log"
"C"
// other imports omitted for brevity
)
func main() {
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// Attach, run, and wait for signals …
}4. Write the user‑space loader ( minimal.go )
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go --go-package minimal -output-dir minimal Minimal minimal.bpf.c -- -I/usr/include/bpf -I/usr/include/linux
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
minimal "github.com/smallnest/ebpf-study/01-helloworld/minimal"
)
func main() {
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatalf("failed to remove memlock: %v", err)
}
objs := minimal.MinimalObjects{}
if err := minimal.LoadMinimalObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
tp, err := link.Tracepoint("syscalls", "sys_enter_write", objs.HandleTp, nil)
if err != nil {
log.Fatalf("opening tracepoint: %v", err)
}
defer tp.Close()
log.Println("eBPF program loaded and attached. Press Ctrl+C to exit.")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
}5. Compile and run
$ go generate
$ go build -o minimal
$ sudo ./minimal
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"The loader removes the default memlock limit, loads the BPF object, attaches it to the sys_enter_write tracepoint, and waits for a termination signal.
Discussion of Core Concepts
Include the appropriate kernel headers ( <linux/bpf.h>, <bpf/bpf_helpers.h>, etc.).
Declare a license (usually "Dual BSD/GPL" or "GPL").
Define a BPF function with a void *ctx argument and an int return value.
Use helper functions such as bpf_get_current_pid_tgid() and bpf_trace_printk() to access kernel data and emit debug output.
Extended Learning
Tracepoints
Tracepoints are static kernel instrumentation points that can be enabled after the fact. They were introduced in Linux 2.6.32 (2009) and provide a stable API. Common ways to list available tracepoints:
Read /sys/kernel/debug/tracing/available_events:
sudo cat /sys/kernel/debug/tracing/available_eventsUse perf list tracepoint: sudo perf list tracepoint Use bpftrace -l 'tracepoint:*': sudo bpftrace -l 'tracepoint:*' Browse the filesystem under /sys/kernel/debug/tracing/events/.
SEC Section Types
The SEC macro tags a function or map with a specific ELF section name, which determines how the kernel treats the program. Common sections include: SEC("tp/syscalls/sys_enter_write") – tracepoint for the write syscall entry. SEC("kprobe/do_sys_open") – kprobe for the kernel function do_sys_open. SEC("xdp") – XDP program that processes network packets. SEC("socket") – socket filter program.
…and many others such as kretprobe, uprobe, cgroup/skb, perf_event, raw_tp, lsm, etc.
Common eBPF CO‑RE Macros
Compile‑Once‑Run‑Everywhere (CO‑RE) macros hide kernel‑version differences. Examples: BPF_CORE_READ(dst, src, field) – safely read a struct field regardless of offset. BPF_CORE_READ_STR_INTO(dst, size, src, field) – read a string field into a buffer. BPF_PROBE_READ(dst, size, src) – generic safe read. bpf_core_type_exists(type) – test if a type exists in the running kernel. bpf_core_field_exists(field) – test if a field exists. BPF_CURRENT_PID_TGID() – obtain current PID/TGID. BPF_GET_CURRENT_COMM(comm) – obtain current process name.
Example using CO‑RE macros in a socket program:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <linux/tcp.h>
SEC("socket")
int socket_prog(struct __sk_buff *skb)
{
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
__u32 pid_tgid = bpf_get_current_pid_tgid();
__u32 pid = pid_tgid >> 32;
char comm[16];
BPF_CORE_READ_STR_INTO(comm, sizeof(comm), task, comm);
struct tcphdr *tcp_header;
tcp_header = bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(struct iphdr), sizeof(struct tcphdr));
if (!tcp_header)
return 0;
__u8 tcp_flags = BPF_CORE_READ_BITFIELD(tcp_header, flags);
bpf_printk("Process %s (PID %d) sent a packet with TCP flags: %x", comm, pid, tcp_flags);
return 0;
}
char LICENSE[] SEC("license") = "GPL";Advanced Example – XDP Packet Filter
The following XDP program drops TCP packets destined for port 80 and passes everything else:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
SEC("xdp")
int xdp_prog(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
__u16 src_port;
BPF_CORE_READ_INTO(&src_port, tcp, source);
if (src_port == bpf_htons(80))
return XDP_DROP;
}
}
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";References
bpf2go – https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go
iovisor/bcc issue #139 – https://github.com/iovisor/bcc/issues/139
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
