In this lesson, you will learn how to write, compile, and load a minimal BPF program to count occurrences of a specific kernel event, such as a process execution.
The Anatomy of a BPF Program
At its core, a BPF program consists of two parts: the BPF bytecode that runs inside the kernel and a userspace loader that manages the BPF map and fetches the collected data.
To count kernel events, we use a BPF_MAP_TYPE_HASH or BPF_MAP_TYPE_ARRAY to store our counters. When the kernel event (the "hook") triggers, our BPF function executes, increments the value associated with a key in our map, and then returns control to the kernel.
Implementing the Counter
Here is a simplified C-style BPF program designed to hook into sys_execve:
#include <linux/bpf.h>
// Define a map to store our counts
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} event_map SEC(".maps");
SEC("kprobe/sys_execve")
int count_exec(void *ctx) {
__u32 key = 0;
__u64 *val = bpf_map_lookup_elem(&event_map, &key);
if (val) {
__sync_fetch_and_add(val, 1);
}
return 0;
}
Compiling and Loading
To move from code to execution, you follow three steps:
- Compilation: Use
clangwith the-target bpfflag to convert the C code into an Executable and Linkable Format (ELF) file containing the BPF bytecode. - Loading: Use a loader (typically using
libbpf) to parse the ELF file, create the map in the kernel, and attach the function to thekprobe. - Observation: Your userspace application performs a
bpf_map_lookup_elemcall on the map’s file descriptor to read the updated counter value.
Because BPF programs are verified by the kernel before execution, you must ensure your logic is finite and avoids illegal memory access—the verifier will reject any program that could potentially hang or crash the system.
Key Takeaway: A BPF program acts as a secure, event-triggered callback that persists data in kernel maps, allowing userspace tools to retrieve real-time telemetry without modifying the kernel source code.