How a tiny program travels from your text into the running kernel — and starts answering your questions.
🎯 Mission: understand the pipeline so you can trust eBPF on a live boxEvery eBPF program follows the same fixed pipeline: compile to BPF bytecode → load with the bpf() syscall → the verifier proves it safe → JIT to native code → attach to a hook → run on every event → results flow out through a map.
In Lesson 1 you met the big picture: eBPF lets you load small, sandboxed programs into the running Linux kernel that fire on an event, so you can watch what the system is really doing — no source changes, no kernel module, no reboot. You also met the four moving parts: the event/hook, the eBPF program, the verifier + JIT safety gate, and the map that carries data out.
This lesson zooms into the verb: how does that program actually get there? It turns out it's the same six-stage assembly line every single time — and once you've seen it, every tracing tool you'll ever use is just a friendly wrapper around these stages.
Pipeline & lifecycle per ebpf.io — What is eBPF? and Brendan Gregg — Linux eBPF Tracing Tools.
An eBPF program starts as text — pseudo-C for a hand-written program, or a one-line script for bpftrace. Either way it gets compiled not into ordinary native machine code, but into BPF bytecode: instructions for a small, made-up processor called the BPF virtual machine that lives inside the kernel.
Think of it like Java or WebAssembly bytecode. The code targets an imaginary CPU with a deliberately tiny, restricted instruction set — a handful of 64-bit registers and simple operations. Hand-written programs are typically compiled with the LLVM/Clang toolchain into this bytecode form, which is what the kernel actually expects to receive.
This is the crux of the whole design. If you handed the kernel raw native instructions, it could do anything — there'd be no way to inspect it for safety, which is exactly the kernel-module problem from Lesson 1. A small, restricted, well-defined instruction set is analyzable: the kernel can read every instruction, follow every branch, and reason about what the program can and can't do before it ever runs.
Source: ebpf.io — "the kernel expects eBPF programs to be loaded in the form of bytecode"; restricted instruction set per docs.kernel.org/bpf.
bpf() syscallOnce you have bytecode, how does it get into the kernel? Through a single dedicated doorway: the bpf() system call. A syscall is the standard, controlled way any user-space program asks the kernel to do something on its behalf — and there's one specifically for "here is an eBPF program, please load it."
You almost never call bpf() by hand. The raw syscall is fiddly, so people use a loader library that wraps it:
The canonical C/C++ library. It reads your compiled object file, sets up maps, and makes the right bpf() calls to load and attach the program.
There are loaders for Go, Rust, Python and more — each abstracts the same bpf() syscall behind a friendlier API for that language.
For tracing you don't touch any of this. bpftrace does the load for you — it compiles your one-liner and calls bpf() behind the scenes. More in section 09.
root (or the CAP_BPF capability). That's why nearly every bpftrace command you'll see is run with sudo.The instant your bytecode arrives via bpf(), it hits the verifier: a piece of the kernel that statically analyzes the program — reading the code without running it — and refuses to load it unless it can prove the program is safe. This is the single most important stage, and the reason eBPF is fundamentally different from a kernel module.
It walks every possible path through your program and checks four families of guarantees:
No infinite loops, no blocking forever. The program must terminate. Loops are only allowed if the verifier can prove they're bounded — that they have a guaranteed exit.
No reading uninitialized variables, no reads or writes out of bounds. It tracks every register and stack slot to be sure each pointer is valid and in-range before use.
The program must stay within strict size and complexity limits — capped at one million analyzed instructions — so verification itself can't take forever.
It can't poke at arbitrary kernel memory. The only sanctioned way to reach kernel facilities is through helper functions — a stable, vetted API (see section 08).
You might wonder how the kernel can prove a loop ends without running it. The verifier explores execution paths and uses state pruning: if it reaches a point where the register/stack state is no longer changing in a way that matters, it knows further iterations add nothing new, so the loop is bounded. If it can't establish that, the program is rejected. (Modern kernels also offer helpers like bpf_loop() for large bounded loops — a detail you won't need for tracing.)
Sources: ebpf.io — verifier guarantees; docs.kernel.org/bpf — the BPF verifier (DAG/loop checks, register & stack state tracking); one-million-instruction complexity limit per the kernel verifier.
A program that's passed the verifier could just be run by an in-kernel interpreter that reads the bytecode instruction by instruction — but that's slow. Instead, on a final pass, a JIT (Just-In-Time) compiler translates the verified BPF bytecode into the native machine instructions of your actual CPU (x86-64, arm64, etc.).
The payoff: your program now runs at roughly the same speed as code compiled directly into the kernel. This is the second half of the "JavaScript for the kernel" analogy from Lesson 1 — a JIT is exactly what makes browser JavaScript fast, and it does the same job here.
| Interpreted bytecode | JIT-compiled (what really happens) | |
|---|---|---|
| What runs on each event | Kernel reads BPF instructions one by one | Real native CPU instructions |
| Speed | Slower (interpreter overhead) | ~Native — like compiled-in kernel code |
| Still verified-safe? | Yes | Yes — JIT only happens after the verifier passes |
Your program is now verified and JIT-compiled, sitting ready in the kernel — but it's not doing anything yet. It needs to be attached to a hook point: a specific spot in the kernel where it should fire. Common hooks for tracing include system calls, kernel function entry/exit (kprobes), user-space function entry/exit (uprobes), and tracepoints (stable, named events the kernel exposes on purpose).
Once attached, execution is event-driven: every time that hook point is reached — every openat() call, every time that function runs — the kernel runs your program, right there, in kernel context, then continues. No polling, no thread of yours waiting around; the program only wakes when its event happens.
An attached program stays loaded until it's detached or until the process that owns it exits — the kernel keeps a reference count and tears the program down when nothing refers to it anymore. For bpftrace, that means: it runs while your command is running, and is cleanly removed the moment you press Ctrl-C.
One-line aside (pinning): you can keep a program or map alive past its loader by pinning it to a special filesystem called bpffs (mounted at /sys/fs/bpf) — a handy detail for long-running tools, but not something you'll need for ad-hoc tracing.
hook.on("openat", yourProgram). The kernel calls your tiny function on every matching event, and removes the listener when you're done.Your program runs deep in the kernel, but the answer you want shows up in your terminal, in user space. How does the data cross back? Through maps. A map is a kernel data structure — a hash table, an array, a histogram, a ring/perf buffer — that is the shared channel between the two worlds.
bpf() syscall — and prints it to you.This is also why maps exist: an eBPF program is short-lived and event-scoped, so it can't keep state on its own or print directly. Maps give it durable, shareable storage that outlives a single event and is visible from outside. (Brendan Gregg notes a program has two ways to pass data back: per-event details, or aggregated via a map.)
Source: ebpf.io — maps are "accessed from eBPF programs as well as from applications in user space via a system call"; Brendan Gregg — per-event vs. map.
Maps are how data gets out. Helper functions are how your program reaches in to kernel facilities. Remember from section 04: the verifier forbids touching arbitrary kernel memory. So how does a program get the current process name, read a timestamp, look up a map entry, or fetch a syscall argument? It calls a helper.
Helpers are a fixed, well-known and stable API offered by the kernel — a curated menu of safe operations. Your program can't invent new ways into the kernel; it can only call from this approved list. That's deliberate: it keeps the safety guarantee intact and keeps programs portable across kernel versions.
The conduit that carries results from kernel space up to your user-space tool.
The only approved way to read kernel state or do privileged work — a stable, vetted list the verifier trusts.
Source: ebpf.io — helper functions are "a well-known and stable API offered by the kernel".
Here's the punchline of the whole lesson. You will almost never perform these stages yourself. bpftrace runs the entire pipeline for you from a single command line: it compiles your one-liner to BPF bytecode, calls bpf() to load it, lets the verifier check it, gets it JIT-compiled, attaches it to the hook you named, runs it on every event, and reads the maps to print results — and then cleanly detaches when you stop it.
# one line — bpftrace does all six pipeline stages for you sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s opened %s\n", comm, str(args->filename)); }'
sudo).Map that single command onto the pipeline you just learned:
| Pipeline stage | What bpftrace does for you |
|---|---|
| ① Compile to bytecode | Turns your one-liner into BPF bytecode |
② Load via bpf() | Calls the syscall to hand it to the kernel |
| ③ Verify | The kernel verifier checks it (you'd see an error if it failed) |
| ④ JIT | The kernel compiles it to native code |
| ⑤ Attach & run | Attaches to sys_enter_openat; fires on every openat |
| ⑥ Maps out | comm/args via helpers; output streams to your terminal |
Source: Brendan Gregg — bpftrace as a high-level front-end that compiles, loads, attaches, and runs BPF for you; one-liner style per bpftrace One-Liner Tutorial.
Pick an answer — you'll get instant feedback. No grading, just a tight feedback loop.
Click to reveal. Come back to these later for quick review.
bpf() syscall → ③ verify (the verifier proves it safe) → ④ JIT to native code → ⑤ attach to a hook → ⑥ run on every event, with results flowing out through a map./sys/fs/bpf).Want a stage drawn out further, or a comparison to something you already know? Just ask in the chat. Good questions to try: "Walk me through what the verifier does to that openat one-liner," or "What's the difference between a kprobe, a uprobe, and a tracepoint as hooks?"