eBPF is a sandboxed virtual machine inside the Linux kernel. It lets you run custom bytecode at kernel hook points without modifying kernel source or loading kernel modules. Programs are verified at load time for safety (no unbounded loops, no invalid memory access), then JIT-compiled to native machine code.
The "extended" part matters: classic BPF (cBPF, 1992) was a simple packet filter with 2 registers and 32-bit ops. eBPF (2014+) is a general-purpose in-kernel execution engine with 11 registers (R0-R10), 64-bit ops, maps (key-value stores), helper functions, kfuncs, and tail calls.
Programs attach to hook points: kprobes (any kernel function), uprobes (any userspace function), tracepoints (stable kernel instrumentation), XDP (network driver ingress), tc (traffic control), cgroup hooks, LSM hooks, and since Linux 6.12 even the CPU scheduler itself. Each hook type defines what context the program receives and what it can do.
Every eBPF program, regardless of frontend, travels the same road: source to bytecode, bytecode through the bpf() syscall, past the verifier, through the JIT, onto a hook.
USERSPACE BCC Python/Lua bpftrace libbpf CO-RE / ebpf-go / aya | | | C source DSL script pre-compiled .o (BTF-relocated) | | | v v v LLVM/Clang compiles to BPF bytecode (or loaded directly) | ------ bpf() syscall -------------------------------------------- | KERNEL v VERIFIER rejects unsafe programs: bounds checks, termination | proof, stack <= 512 bytes, no wild pointer derefs * v JIT COMPILER bytecode -> native (x86_64, arm64, riscv64, ...) | v ATTACH to hook point (kprobe, tracepoint, XDP, LSM, ...) | v BPF MAPS <-- shared state between kernel prog & userspace (hash, array, ringbuf, perf_event, LRU, stack_trace, arena, ...) | v Userspace reads maps / ring buffer / perf buffer for output * bounded loops allowed since 5.3 (verifier proves termination); bpf_loop since 5.17, open-coded iterators since 6.4
clang -target bpf). BCC does this at runtime on the target machine; libbpf-style toolchains do it once at build time.CAP_BPF (or root; delegable via BPF token since 6.9).Every BPF program passes through the verifier before execution. It walks all possible code paths and proves:
The complexity budget is 1 million verified instructions per program (since 5.2; it was 4096 in the early days). The verifier is your biggest adversary when writing BPF C: R1 invalid mem access 'scalar' means you dereferenced a raw kernel pointer without bpf_probe_read_kernel(). In kprobe context, every struct field read from a kernel pointer needs an explicit probe read (BCC rewrites most of them for you; see BCC Anatomy).
Maps are the shared data structures between kernel-side BPF programs and userspace. They persist across program invocations and are the primary mechanism for collecting data.
| Map type | What it is | Notes |
|---|---|---|
| BPF_MAP_TYPE_HASH | General key/value store | The workhorse. BPF_HASH() in BCC. |
| BPF_MAP_TYPE_ARRAY | Fixed-size, indexed by u32 | Preallocated, fastest lookup. |
| BPF_MAP_TYPE_PERF_EVENT_ARRAY | Per-CPU event stream to userspace | BPF_PERF_OUTPUT() in BCC. What the lab tools use. |
| BPF_MAP_TYPE_RINGBUF | Single shared ring buffer | 5.8+. Preferred over perf buffers: lower overhead, ordered, no per-CPU sizing. |
| BPF_MAP_TYPE_LRU_HASH | Hash with automatic eviction | For caches that must not fill up. |
| BPF_MAP_TYPE_PERCPU_HASH / _ARRAY | Per-CPU copies, lock-free writes | Aggregate in userspace. Counters at high event rates. |
| BPF_MAP_TYPE_STACK_TRACE | Call stack capture | Feeds flame graphs. |
| BPF_MAP_TYPE_PROG_ARRAY | Program references | Tail calls: one BPF program jumps to another. |
| BPF_MAP_TYPE_ARENA | Sparse shared memory region | 6.9+. Kernel and userspace share pointers into one address space. See Modern eBPF. |
BPF_PERF_OUTPUT + open_perf_buffer) because the BCC Python API for them is mature and simple. New code targeting 5.8+ should default to BPF_MAP_TYPE_RINGBUF: one buffer instead of one per CPU, events arrive in order, and memory is reserved in-place instead of copied twice.
The hook point determines the program type, the context struct it receives, and which helpers it may call. Three broad families: tracing, networking, security.
| Hook | Reaches | Notes |
|---|---|---|
| kprobe / kretprobe | Any kernel function entry/return | Maximum reach, zero stability guarantees. |
| uprobe / uretprobe | Any userspace function entry/return | Hook libc, your own binaries, interpreters. Multi-attach since 6.6. |
| tracepoint | Stable kernel instrumentation points | Survives kernel upgrades. Listed in /sys/kernel/debug/tracing/events. |
| raw_tracepoint | Same points, raw arguments | Lower overhead, no argument casting done for you. |
| fentry / fexit | Any kernel function via BPF trampoline | 5.5+. BTF-typed args, much lower overhead than kprobes. Modern default where available. |
| perf_event | PMC sampling, timers | CPU profiling: sample stacks N times per second. |
| Hook | Where | Notes |
|---|---|---|
| XDP | Network driver ingress, before skb allocation | Fastest path. Actions: PASS, DROP, TX, REDIRECT. |
| tc (clsact) | Traffic control ingress/egress | Full skb access, both directions. Cilium's main datapath. |
| sk_filter | Socket-level filtering | The classic tcpdump attach point, extended. |
| sk_msg / sk_skb | Sockmap redirect | Socket-to-socket splicing for service meshes. |
| cgroup/sock* | Per-cgroup socket operations | Per-container network policy, connect/bind interposition. |
| struct_ops | Kernel subsystem callbacks | 5.6+. Entire TCP congestion control algorithms in BPF. |
| Hook | What it controls | Notes |
|---|---|---|
| LSM (BPF LSM) | Security module hooks | 5.7+. Enforce policy at the same points as SELinux/AppArmor, loaded dynamically. |
| cgroup/device | Device access per cgroup | Container device whitelisting. |
| cgroup/sysctl | sysctl reads/writes per cgroup | Interpose and deny. |
BCC (BPF Compiler Collection) provides a Python (and Lua) frontend for writing BPF programs. The kernel-side C is embedded as a string, compiled at runtime via LLVM, and loaded into the kernel. This is the fastest path from zero to working instrumentation, and it is what every tool in this repo uses.
| Strengths | Trade-offs |
|---|---|
| Rapid prototyping: write, run, iterate in seconds | Requires LLVM + kernel headers on every target machine |
| Rich Python API for maps, perf buffers, formatting | Compiles at runtime: slow startup, noticeable on small ARM boards |
| 100+ battle-tested bundled tools (execsnoop, biolatency, ...) | Programs are tied to the kernel they compiled against (no CO-RE) |
| Automatic struct offset resolution from kernel headers | Heavy install, roughly 300 MB with LLVM |
libbpf-tools, pre-compiled rewrites of its classic tools. See Toolchains.
BCC Python #!/usr/bin/env python3 from bcc import BPF # ---- kernel side (C) ---- prog = """ struct event_t { u32 pid; char comm[16]; char fname[64]; u64 size; }; BPF_PERF_OUTPUT(events); // declare perf buffer int trace_read_entry(struct pt_regs *ctx, struct file *file, char __user *buf, size_t count) { struct event_t evt = {}; evt.pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(&evt.comm, sizeof(evt.comm)); bpf_probe_read_kernel_str(&evt.fname, sizeof(evt.fname), file->f_path.dentry->d_name.name); evt.size = count; events.perf_submit(ctx, &evt, sizeof(evt)); return 0; } """ # ---- userspace side (Python) ---- b = BPF(text=prog) # compile + load b.attach_kprobe(event="vfs_read", fn_name="trace_read_entry") # attach to hook def handle_event(cpu, data, size): # callback for events evt = b["events"].event(data) print(f"{evt.pid} {evt.comm} {evt.fname} {evt.size}") b["events"].open_perf_buffer(handle_event) while True: b.perf_buffer_poll() # blocks, calls handle_event
ntohs(sk->field) or reads through multiple pointer hops inside expressions). That is when you hit verifier errors and need an explicit bpf_probe_read_kernel().
Every script in this repository is a runnable BCC program that demonstrates one or two of the concepts above. Run them with root (or CAP_BPF + CAP_PERFMON), roughly in this order.
| Tool | Hooks | Maps | What it teaches |
|---|---|---|---|
| 01_hello_execsnoop.py | kprobe on sys_execve arch wrapper | perf buffer | Smallest possible tracer: see every exec on the box. |
| 02_syscall_counter.py | tracepoint raw_syscalls:sys_enter | hash | In-kernel aggregation: count instead of stream. |
| new_processes.py | tracepoints sys_enter/exit_execve | hash + perf buffer | Entry/exit correlation to capture args and return value. |
| file_watcher.py | tracepoints openat, read, write | perf buffer | Filtering events per PID; watching one process's file activity. |
| file_walker.py | none (no BPF) | none | Workload generator: a predictable target for file_watcher. |
| io_scope.py | tracepoints openat, read, write | perf buffer | Full I/O profile of a process: files, bytes, rates. |
| slow_syscalls.py | tracepoints raw_syscalls enter + exit | hash + perf buffer | Latency measurement: timestamp on entry, delta on exit. |
| tcp_connect.py | kprobe + kretprobe tcp_v4_connect | hash + perf buffer | kretprobe pattern: stash the socket on entry, read the result on return. |
| tcp_latency.py | kprobes tcp_v4_connect, tcp_finish_connect | hash + perf buffer | Handshake RTT by correlating two different kernel functions. |
| conn_map.py | kprobes tcp_sendmsg, tcp_cleanup_rbuf | hash | Per-connection byte accounting in a keyed map. |
| net_scope.py | five TCP kprobes | hashes + 2 perf buffers | Connection lifecycle: connect, latency, bytes, close, in one tool. |
| dns_snoop.py | kprobe udp_sendmsg | perf buffer | Reading packet payloads: parse DNS queries in flight. |
| port_scan_detect.py | kprobe tcp_v4_conn_request | perf buffer | Security angle: spot inbound SYN patterns that look like scans. |
| mem_scope.py | tracepoints brk/mmap/munmap + kprobe handle_mm_fault | hashes + perf buffer | Memory behavior: allocations and page faults per process. |
__x64_sys_execve on x86_64, __arm64_sys_execve on arm64. 01_hello_execsnoop.py attaches to the arm64 symbol; on an x86_64 machine change the attach line (or use b.get_syscall_fnname("execve"), which resolves it portably). The tracepoint-based tools are immune to this, which is exactly the kprobe-vs-tracepoint stability trade-off from Hook Points.
| Helper | Purpose |
|---|---|
| bpf_get_current_pid_tgid() | Returns u64: TGID (the userspace "PID") in the upper 32 bits, thread ID in the lower. |
| bpf_get_current_comm(buf, sz) | Process name, 16 chars max. |
| bpf_ktime_get_ns() | Monotonic nanoseconds. The clock for latency measurements. |
| bpf_probe_read_kernel(dst, sz, src) | Safe kernel memory read. The fix for most verifier errors. |
| bpf_probe_read_user(dst, sz, src) | Safe userspace memory read (syscall args, buffers). |
| bpf_probe_read_kernel_str() / _user_str() | Read NUL-terminated strings safely. |
| bpf_map_lookup_elem(map, &key) | Read from map. Returns pointer or NULL; the verifier forces you to check. |
| bpf_map_update_elem(map, &key, &val, flags) | Write to map (BPF_ANY / BPF_NOEXIST / BPF_EXIST). |
| bpf_map_delete_elem(map, &key) | Remove from map. |
| bpf_perf_event_output(ctx, map, flags, data, sz) | Send an event to a perf buffer. perf_submit() in BCC. |
| bpf_ringbuf_reserve() / _submit() | Zero-copy ring buffer output (5.8+). The modern replacement for the above. |
| bpf_trace_printk(fmt, ...) | Debug only: slow, limited, output lands in /sys/kernel/debug/tracing/trace_pipe. |
The old bpf_probe_read() still exists for compatibility but has been split into the _kernel/_user variants since 5.5; new code should always say which address space it means.
C side (embedded string) BPF_HASH(name, key_t, val_t) // hash map BPF_ARRAY(name, val_t, max) // array map BPF_PERF_OUTPUT(name) // perf buffer BPF_PERCPU_ARRAY(name) // per-cpu array BPF_RINGBUF_OUTPUT(name, pgs) // ring buffer TRACEPOINT_PROBE(cat, name) // tracepoint handler
Python side b.attach_kprobe(event, fn_name) b.attach_kretprobe(event, fn_name) b.attach_uprobe(name, sym, fn_name) b.get_syscall_fnname("execve") # arch-portable b["map"].open_perf_buffer(cb) b.perf_buffer_poll() # block + dispatch b["map"][key] # direct map read
| Domain | Function | Fires on |
|---|---|---|
| File I/O | vfs_read / vfs_write | All file reads/writes, any filesystem. |
| File I/O | vfs_open | File opens at the VFS layer. |
| File I/O | do_sys_openat2 | The openat syscall implementation. |
| Network | tcp_v4_connect / tcp_v6_connect | Outbound TCP connection attempts. |
| Network | tcp_finish_connect | Handshake completion. |
| Network | tcp_sendmsg / tcp_cleanup_rbuf | TCP bytes out / bytes consumed in. |
| Network | tcp_close | Connection teardown. |
| Network | udp_sendmsg | UDP out (DNS lives here). |
| Process | __x64_sys_execve / __arm64_sys_execve | exec, per architecture (see Lab Tools). |
| Process | do_exit | Process exit. |
| Process | wake_up_new_task | fork/clone: new task becomes runnable. |
| Memory | handle_mm_fault | Page faults. |
The verifier's error messages are terse and the line numbers refer to bytecode, not your C. These are the ones you will actually hit, and what they mean.
| Error | Cause | Fix |
|---|---|---|
| R1 invalid mem access 'scalar' | Dereferencing a raw kernel pointer directly. | Read through bpf_probe_read_kernel(). |
| BPF stack limit exceeded (512 bytes) | Local struct too large for the BPF stack. | Use a BPF_PERCPU_ARRAY of size 1 as scratch space, or bpf_ringbuf_reserve() and build the event in the ring buffer. |
| back-edge from insn X to Y | Loop the verifier cannot bound (always fatal before 5.3). | #pragma unroll, a provably bounded loop on 5.3+, or bpf_loop() on 5.17+. |
| invalid indirect read from stack | Passing uninitialized stack memory to a helper. | Zero-initialize: struct foo bar = {}; |
| map_value or possibly-null pointer | Using a bpf_map_lookup_elem() result without a NULL check. | if (!val) return 0; immediately after the lookup. |
| BPF program is too large / processed N insns | Blew the 1M verified-instruction complexity budget. | Simplify branches, bound loops tighter, split logic across tail calls. |
| btf_vmlinux is malformed / no BTF found | Kernel BTF data missing. | Kernel must be built with CONFIG_DEBUG_INFO_BTF=y (standard on modern distro kernels; check for /sys/kernel/btf/vmlinux). |
Five realistic ways to write eBPF in 2026. The split that matters is runtime compilation (BCC) versus compile-once-run-everywhere (everything else, via BTF + CO-RE).
| Stack | Language | Compile model | Startup | Portability | Best for |
|---|---|---|---|---|---|
| BCC | Python/Lua + embedded C | LLVM on target, at runtime | Slow | Tied to build kernel | Learning, prototyping, one-off investigation. |
| libbpf + CO-RE | C | Ahead of time, BTF relocations | Instant | Cross-kernel | Production tools, minimal footprint. The reference implementation. |
| ebpf-go (cilium/ebpf) | Go userspace, C kernel side | Ahead of time, bpf2go embeds bytecode | Instant | Cross-kernel | Go services and infrastructure tooling. Powers Cilium. |
| aya | Rust on both sides | Ahead of time, pure-Rust loader (no libbpf) | Instant | Cross-kernel | Rust shops; kernel + userspace in one language and one cargo build. |
| bpftrace | awk-like DSL | On the fly (libbpf-based since 0.21) | Moderate | Needs BTF | One-liners and ad-hoc tracing. See bpftrace. |
cilium/ebpf (ebpf-go) is the pure-Go library for loading and managing pre-compiled BPF programs. Write BPF C, compile it with clang to a .o at build time, then load, attach, and read maps from Go. No LLVM on the target machine; CO-RE + BTF handles cross-kernel portability.
workflow # 1. write the kernel side probe.bpf.c # 2. generate Go bindings + embed bytecode (wraps clang -target bpf) $ go generate ./... # runs bpf2go # 3. load and attach from Go objs := probeObjects{} loadProbeObjects(&objs, nil) link.Kprobe("tcp_v4_connect", objs.TraceConnect, nil) # 4. read events rd, _ := ringbuf.NewReader(objs.Events) record, _ := rd.Read()
This is what Cilium itself uses for Kubernetes networking, service mesh, and observability (Hubble), and what Cloudflare, Meta, and Datadog build production eBPF tooling with. If you are building an interactive eBPF tool in Go, this is the load-bearing dependency; the engine loads compiled programs, and bpf2go keeps the C and Go sides in sync at build time.
bpftrace is the awk of kernel tracing: probes, predicates, and actions in a one-line DSL, compiled to eBPF under the hood. Since 0.21 it is built on libbpf and uses your kernel's BTF, so no headers are needed on a modern distro.
bpftrace # count syscalls by process $ bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' # trace file opens with path $ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }' # histogram of read() sizes $ bpftrace -e 'tracepoint:syscalls:sys_exit_read /args.ret > 0/ { @bytes = hist(args.ret); }' # TCP connect with destination IP $ bpftrace -e 'kprobe:tcp_v4_connect { printf("%s -> %s\n", comm, ntop(((struct sock *)arg0)->__sk_common.skc_daddr)); }' # slow syscalls (>1ms) $ bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; } tracepoint:raw_syscalls:sys_exit /@start[tid]/ { $d = nsecs - @start[tid]; if ($d > 1000000) { printf("%s %dms\n", comm, $d / 1000000); } delete(@start[tid]); }' # list every probe you could attach to $ bpftrace -l 'tracepoint:syscalls:*'
Note the parallels: the slow-syscalls one-liner is slow_syscalls.py from Lab Tools in three lines, and the openat trace is the core of file_watcher.py. bpftrace is the fastest way to test a hypothesis before writing the full tool.
Everything earlier in this guide worked in 2020. This is what the kernel added since, roughly in order of appearance. If your kernel is 6.12 or newer (this repo's reference machine runs 6.17), all of it is available.
| Feature | Kernel | What it unlocks |
|---|---|---|
| BPF trampoline, fentry/fexit | 5.5 | Function tracing with BTF-typed arguments at a fraction of kprobe cost. |
| struct_ops | 5.6 | Implement kernel subsystem callbacks in BPF; first user was TCP congestion control. |
| BPF LSM | 5.7 | Dynamic security policy on LSM hooks (the KRSI work). |
| Ring buffer | 5.8 | Ordered, low-overhead event stream replacing per-CPU perf buffers. |
| Sleepable programs | 5.10 | Programs that may fault-in user memory; required for many LSM uses. |
| bpf_loop() | 5.17 | Helper-driven loops with verifier-friendly bounds. |
| kfuncs | 5.18+ | Kernel functions exposed directly to BPF. The helper set is frozen; kfuncs are how new kernel functionality reaches BPF now. |
| uprobe multi-attach | 6.6 | Attach one program to thousands of userspace symbols cheaply. |
| BPF exceptions | 6.7 | bpf_throw(): assert invariants and bail out, instead of contorting code to satisfy the verifier. |
| BPF arena | 6.9 | Sparse memory region shared between BPF and userspace; build real data structures (trees, custom allocators) spanning both. |
| BPF token | 6.9 | Delegate scoped BPF privileges to unprivileged processes (containers) via the BPF filesystem, instead of granting CAP_BPF broadly. |
| sched_ext | 6.12 | Entire CPU scheduling policies as loadable BPF programs, with the kernel reverting to the default scheduler on any misbehavior. |
| Year | Milestone | Detail |
|---|---|---|
| 1992 | BPF introduced | Steven McCanne and Van Jacobson. Packet filter for BSD; 2 registers, 32-bit. The engine under tcpdump. |
| 2014 | eBPF merged (Linux 3.15-3.18) | Alexei Starovoitov, with Daniel Borkmann. 11 registers, 64-bit, maps, verifier, JIT; the bpf() syscall lands in 3.18. |
| 2015 | kprobe attach + BCC | BPF programs can attach to kernel functions (4.1). The BCC project starts at PLUMgrid. |
| 2016 | XDP merged (4.8), Cilium appears | BPF at the network driver level. Thomas Graf and team start Cilium for container networking. |
| 2018 | BTF (4.18) | Type metadata in the kernel enables CO-RE: compile once, relocate at load time, run on any kernel. |
| 2019 | Bounded loops (5.3), bpftrace 0.9 | The verifier learns to prove loop termination. High-level tracing goes mainstream. |
| 2020 | fentry (5.5), LSM (5.7), ringbuf (5.8) | Low-overhead tracing, dynamic security policy, and the modern event transport, all in one year. |
| 2021 | eBPF Foundation, eBPF for Windows | Meta, Google, Isovalent, Microsoft, and Netflix found the eBPF Foundation under the Linux Foundation (August). Microsoft announces the eBPF-for-Windows runtime (May). |
| 2022-23 | kfuncs era, exceptions (6.7) | The helper interface freezes; kfuncs become the extension path. BPF exceptions land. |
| 2024 | Arena + token (6.9), sched_ext (6.12) | Shared memory with userspace, delegable privileges, and BPF-defined CPU scheduling. See Modern eBPF. |
Where to go once the lab tools feel small. Each of these leads to better questions.
| Topic | Why it matters | Where to start |
|---|---|---|
| ebpf.io + docs.ebpf.io | The ecosystem map and the best-organized reference for program types, helpers, and kfuncs. | ebpf.io "What is eBPF", then docs.ebpf.io per program type |
| BPF Performance Tools | Brendan Gregg's book. The methodology (USE, latency heat maps) matters as much as the tools. | The book, plus the tool source in the BCC repo it documents |
| libbpf-bootstrap | Minimal scaffolding for real CO-RE tools in C, Go, and Rust. The step after BCC. | github.com/libbpf/libbpf-bootstrap |
| BPF features by kernel version | The canonical table of which helper/map/program type landed when. Answers "can my target kernel do X". | docs/kernel-versions.md in the BCC repo |
| Verifier internals | Understanding register state tracking and pruning turns verifier errors from mysteries into compiler diagnostics. | kernel Documentation/bpf/verifier.rst, LWN's BPF coverage |
| bpftool | Inspect everything: loaded programs, maps, BTF, JIT output. The debugger you already have. | bpftool prog list, bpftool prog dump xlated id N, bpftool btf dump file /sys/kernel/btf/vmlinux |
| sched_ext schedulers | Real, running BPF CPU schedulers you can read, load, and break safely (the kernel falls back on error). | github.com/sched-ext/scx |
| Tetragon & Falco | Production security observability built on the tracing + LSM hooks from this guide. | tetragon.io, falco.org |
| eunomia-bpf tutorials | Worked examples for the newest features: arena, exceptions, tokens, GPU tracing experiments. | eunomia.dev/tutorials |