eBPF Course · Lesson 2

Life of an eBPF Program

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 box
The one idea

Every 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.

01 Recap, then the whole journey at a glance

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.

USER SPACE Your source (or a bpftrace line) Output you read "bash opened /etc/passwd" — bpf() syscall boundary — KERNEL SPACE ① BYTECODE restricted instruction set ② VERIFIER gate ✓ / ✗ ✗ rejected → never loads ③ JIT → native machine code ④ ATTACH to a hook (e.g. a syscall) ⑤ RUN on every event, in-kernel event ↻ event ↻ event ⑥ MAP shared table — the data conduit compile + load ↓ read ↑
The fixed six-stage pipeline, left to right: ① compile to bytecode · ② the verifier gate (pass or the program never loads) · ③ JIT to native code · ④ attach to a hook · ⑤ run on every event (it loops) · ⑥ results flow out through a map to your tool.
🎯 Why you careKnowing these stages is what lets you trust eBPF on a production server. The verifier gate (②) is the reason a buggy program is a harmless error message instead of an outage — and the JIT (③) is why watching can be almost free.

Pipeline & lifecycle per ebpf.io — What is eBPF? and Brendan Gregg — Linux eBPF Tracing Tools.

02 From source to BPF bytecode

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.

Why bytecode, and not arbitrary native code?

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.

💡 The key tradeThe restricted instruction set is a feature, not a limitation. By giving up "run any code you want," eBPF buys the ability to mathematically check the code is safe — the foundation of running it live in the kernel.

Source: ebpf.io — "the kernel expects eBPF programs to be loaded in the form of bytecode"; restricted instruction set per docs.kernel.org/bpf.

03 Loading via the bpf() syscall

Once 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:

libbpf

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.

Language libraries

There are loaders for Go, Rust, Python and more — each abstracts the same bpf() syscall behind a friendlier API for that language.

bpftrace (your tool)

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.

🔒 Loading is privilegedLoading an eBPF program is powerful, so it needs root (or the CAP_BPF capability). That's why nearly every bpftrace command you'll see is run with sudo.

Source: ebpf.io — "loaded into the Linux kernel using the bpf system call … typically done using one of the available eBPF libraries".

04 The verifier, in depth — the safety gate

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.

What the verifier proves

It walks every possible path through your program and checks four families of guarantees:

① It always finishes

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.

② Memory is always valid

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.

③ It fits the budget

The program must stay within strict size and complexity limits — capped at one million analyzed instructions — so verification itself can't take forever.

④ Kernel data only via helpers

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).

How "no infinite loops" actually works

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.)

⚠️ Fail the proof → it never loadsIf the verifier can't prove safety, the program is rejected at load time with an error. Nothing runs; the kernel is untouched. There is no "load it anyway and hope." This is the whole safety story in one sentence: an unsafe eBPF program is a load error, not a crash.
🎯 Why this matters for youBecause the verifier guarantees termination and memory safety before anything executes, you can attach a tracing program to a hot kernel path on a live production server without risking an outage. That guarantee is what makes the whole mission possible.

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.

05 JIT compilation — bytecode becomes native speed

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 bytecodeJIT-compiled (what really happens)
What runs on each eventKernel reads BPF instructions one by oneReal native CPU instructions
SpeedSlower (interpreter overhead)~Native — like compiled-in kernel code
Still verified-safe?YesYes — JIT only happens after the verifier passes
💡 Order mattersVerify first, JIT second. The safety proof is done on the bytecode; only proven-safe code is ever compiled to native and allowed to run. You get safety and speed, not a choice between them.

Source: ebpf.io — "the JIT compilation step translates the generic bytecode … into the machine specific instruction set to optimize execution speed".

06 Attach to a hook & run on every event

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.

How long does it stay?

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.

💡 Mental modelAttaching is like adding an event listener in JavaScript: hook.on("openat", yourProgram). The kernel calls your tiny function on every matching event, and removes the listener when you're done.

Source: ebpf.io — hooks & event-driven execution (syscalls, function entry/exit, tracepoints, kprobes/uprobes).

07 Maps — the data conduit out

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.

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.

08 Helper functions — the only sanctioned API in

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.

Maps = data OUT

The conduit that carries results from kernel space up to your user-space tool.

Helpers = sanctioned API IN

The only approved way to read kernel state or do privileged work — a stable, vetted list the verifier trusts.

💡 Remember it as a pairMaps carry data out; helpers are the sanctioned API in. Together they're the program's entire connection to the rest of the system — everything else is walled off.

Source: ebpf.io — helper functions are "a well-known and stable API offered by the kernel".

09 Where bpftrace fits — all six stages from one line

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));
                }'
Linux-only — copy and try later on a Linux box (needs sudo).

Map that single command onto the pipeline you just learned:

Pipeline stageWhat bpftrace does for you
① Compile to bytecodeTurns your one-liner into BPF bytecode
② Load via bpf()Calls the syscall to hand it to the kernel
③ VerifyThe kernel verifier checks it (you'd see an error if it failed)
④ JITThe kernel compiles it to native code
⑤ Attach & runAttaches to sys_enter_openat; fires on every openat
⑥ Maps outcomm/args via helpers; output streams to your terminal
🎯 The takeawayEverything in this lesson happens under the hood of one bpftrace line. You don't need to drive the pipeline — but knowing it's there is what lets you reason about overhead, safety, and why a command behaves the way it does. In Lesson 3 you'll dissect that one-liner piece by piece.

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.

10 Check yourself

Pick an answer — you'll get instant feedback. No grading, just a tight feedback loop.

1. Which of these does the verifier guarantee before a program is allowed to load?
2. Why is the verified bytecode JIT-compiled?
3. What are maps for?
4. When you run a bpftrace one-liner, how much of the pipeline do you perform by hand?

11 Flashcards

Click to reveal. Come back to these later for quick review.

Q: What are the six stages an eBPF program passes through, in order?
A: ① compile to BPF bytecode → ② load via the 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.
Q: Why is an eBPF program compiled to bytecode for a virtual machine instead of arbitrary native code?
A: A small, restricted instruction set is analyzable — the kernel can read every instruction and prove the program is safe before running it. Arbitrary native code couldn't be checked, which is the kernel-module problem eBPF avoids.
Q: What four things does the verifier prove?
A: (1) the program always terminates (no infinite loops; loops must be bounded), (2) it never reads uninitialized or out-of-bounds memory, (3) it fits strict size/complexity limits (capped at ~1 million analyzed instructions), and (4) it reaches kernel data only through stable helper functions. If any proof fails, it never loads.
Q: What is the JIT step, and why does it run after the verifier?
A: The JIT (Just-In-Time) compiler translates the verified bytecode into native machine instructions so the program runs at roughly native speed. It runs after verification so only proven-safe code is ever compiled and executed — safety and speed.
Q: What's the difference between a map and a helper function?
A: A map is the data conduit out — a shared structure both the kernel program and your user-space tool can read/write. A helper is the sanctioned API in — the only approved, stable way for the program to reach kernel facilities (since it can't touch arbitrary memory).
Q: After a program is attached, when does it run, and how long does it stay?
A: It runs event-driven — in kernel context, every time its hook point is reached. It stays loaded until detached or until the owning process exits (the kernel reference-counts it). It can be kept alive longer by pinning it to bpffs (/sys/fs/bpf).
👩‍🏫 I'm your teacher — ask me anything

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?"