eBPF programs are compiled C code, but they’re not just any C. They’re a restricted subset, designed to run safely and efficiently within the Linux kernel.
Let’s see a simple eBPF program in action. This program attaches to a tracepoint and prints a message whenever a sys_enter_execve event occurs, which is when a program is about to be executed.
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
SEC("tp/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx) {
pid_t pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("execve called by PID: %d\n", pid);
return 0;
}
char _license[] SEC("license") = "GPL";
To compile and run this, you’d typically use clang to compile it into an eBPF object file, and then bpftool or a custom loader (often written with libbpf) to load and attach it to the kernel.
# Compile using clang
clang -target bpf -O2 -c execve_trace.c -o execve_trace.o
# Load and attach (example using bpftool, though libbpf loaders are more common for complex apps)
# bpftool prog load execve_trace.o /sys/fs/bpf/execve_trace
# bpftool prog attach pinned:execve_trace tp:syscalls:sys_enter_execve
# View output (requires root and a way to read bpf_printk output, e.g., trace-cmd or another eBPF program)
# trace-cmd record -e 'syscalls:sys_enter_execve'
# trace-cmd report
The SEC("...") macro is crucial. It tells the eBPF verifier and loader which section the eBPF code belongs to. tp/syscalls/sys_enter_execve specifically targets the sys_enter_execve tracepoint. The struct trace_event_raw_sys_enter *ctx is the context passed to the program, containing information about the syscall event. bpf_get_current_pid_tgid() is a helper function provided by the eBPF runtime to get the process and thread ID. bpf_printk is another helper that writes to a buffer, accessible via tools like trace-cmd or bpftool prog runh. The license section is mandatory for eBPF modules.
The power of eBPF comes from its ability to hook into numerous kernel events: tracepoints, kprobes, uprobes, network events, and more. Each hook point provides a specific context (ctx) that your eBPF program can inspect. The libbpf library simplifies this by providing a robust framework for loading eBPF programs, managing BPF maps (data structures shared between kernel and userspace), and handling program attachment.
When you write eBPF in C, you’re essentially writing functions that the kernel will execute. The eBPF verifier is a critical component that analyzes your program before it runs in the kernel. It ensures your program is safe: it won’t crash the kernel, it won’t loop infinitely, and it only accesses valid memory. This verification process is why you can’t use arbitrary C features like dynamic memory allocation or unbounded loops. You’re limited to a specific set of BPF instructions and helper functions.
The vmlinux.h header, often generated by tools like bpftool bcc, provides kernel type definitions, making it easier to work with the context structures. bpf/bpf_helpers.h contains the declarations for the BPF helper functions.
This tracepoint example is just the tip of the iceberg. You can write eBPF programs to monitor network traffic, profile application performance, enforce security policies, and much more, all without modifying kernel source code or loading kernel modules.
The bpf_printk function is a debugging convenience. For production systems, you’d typically use BPF maps to communicate data back to userspace. Maps are key-value stores that can hold various data types, allowing your eBPF program to collect metrics, counters, or even complex state information and make it available to userspace applications.
The actual execution flow involves clang compiling your C code into BPF bytecode. This bytecode is then loaded by libbpf (or another loader) into the kernel. The kernel’s eBPF verifier checks the bytecode for safety. If it passes, the bytecode is JIT-compiled into native machine code and attached to the specified hook point. When the kernel event occurs, your JIT-compiled eBPF code runs.
The most surprising thing about eBPF programs written in C is how much functionality you can achieve with such strict limitations. You can, for instance, implement sophisticated traffic shaping or network filtering logic entirely in eBPF, bypassing expensive userspace context switches for every packet.
The next step is often learning about BPF maps and how to use them to pass data from your eBPF program back to userspace for analysis or action.