TL;DR: eBPF lets you run small, sandboxed programs inside the Linux kernel — to observe or change what the system is doing — without patching the kernel or modifying your apps. If you've ever run
tcpdump, you've already used its ancestor.
The problem: you can't easily change the kernel, and a new capability can take years to land
There's a famous cartoon by Liz Rice that nails the old pain:
- An app developer wants a new capability from the kernel.
- The kernel maintainer says: "Sure — give me a year to convince the whole community."
- A year later it's upstream. A few years after that, your distro finally ships it.
- By then… your requirements have changed.

Source: Learning eBPF — Liz Rice
The kernel sees almost everything that happens on the system — every syscall, every packet, every file operation. And it's open source: you can read the code in as much detail as you like. So the hard part was never seeing inside — it's changing it: safely growing the exact capability you need into a kernel that's already serving production traffic. Historically there were only two awkward paths. One: patch the kernel source, recompile, reboot — slow, risky, and you still have to convince upstream to take your patch. Two: bolt on a user-space agent that pokes from the outside by polling /proc and /sys — easy to install, but blind to first-hand events inside the kernel. eBPF is the third way.
What you'll learn
- What eBPF actually is, in one sentence
- Why it exists — told through its own history (
tcpdump→ today) - How it works in 30 seconds, and when to reach for it (and when not to)
A one-sentence mental model
eBPF is a safe, sandboxed virtual machine inside the Linux kernel that runs your tiny programs in response to events — a packet arrives, a syscall fires, a function is called — letting you observe or control the system without changing kernel source or your application.
It's a mechanism for safely loading kernel extensions at runtime. Every tiny program you attach is a hook bound to some event — but unlike the "write whatever you want" scripts in a browser, every eBPF program must first pass the kernel's built-in verifier: a static check that strictly bounds which instructions it may use, how many loop iterations it may run, and what memory it may touch. That constraint isn't red tape — it's the very thing that lets it run safely inside a live kernel.
Why it exists: from Classic BPF to eBPF
1992 — Classic BPF (you've already used it). BPF was born as a packet filter. Every time you run:
tcpdump -i eth0 tcp port 80
…this happens under the hood:
-
tcpdumphands your filter (tcp port 80) to libpcap - libpcap compiles it into Classic BPF bytecode
- A syscall pushes that bytecode down into the kernel
- The kernel runs it at the network layer and only hands matching packets back up
That's the whole idea, already present in 1992: push a small program down into the kernel so the work happens there, not in your app. (You can even see the compiled bytecode with tcpdump -ddd.)
~2014 — extended BPF (eBPF). The same idea got generalized far beyond packets. Now you can attach tiny programs to almost anything in the kernel — syscalls, function entry/exit, the scheduler, memory, the whole network stack. Brendan Gregg's well-known BPF performance tools map shows how far it spread: there's an eBPF-powered tool for nearly every layer of the kernel.
How it works (the 30-second version)
Your C code
│ clang / LLVM
▼
eBPF bytecode (.o, an ELF object)
│ bpf() syscall (via libbpf / bpftool / BCC)
▼
┌──────────────────── Kernel ────────────────────┐
│ 1. Verifier → checks it's safe │
│ (no crashes, no infinite loops) │
│ 2. (JIT) → runs in the BPF virtual machine │
│ 3. Attached to a hook: │
│ tracepoint / kprobe / XDP / tc / ... │
└───────────────────┬─────────────────────────────┘
│ maps / ring buffer
▼
Your user-space app reads the data
Two things make this both safe and practical:
- The verifier rejects any program that could crash or hang the kernel before it runs. This is the reason eBPF is safe to run in production.
- maps & ring buffers are how the kernel-side program and your user-space program share data.
Where you can attach — the SEC(...) you'll see in real code:
| Hook | Level | Used for |
|---|---|---|
xdp |
NIC driver | fastest packet processing, DDoS drop, load balancing |
tc |
traffic control | packet shaping, classification |
tracepoint/... |
kernel tracepoints | syscalls, file ops, scheduling events |
fentry/fexit
|
kernel functions (CO-RE) | low-overhead function tracing |
usdt |
user space | trace inside apps (e.g. SQL query time) |
How people actually write it (pick your entry point)
You rarely touch raw bytecode. The common toolchains:
| Toolchain | Language | Best for |
|---|---|---|
| C + libbpf | C | production; CO-RE (vmlinux.h + BTF) → portable, compile-once-run-anywhere |
| BCC | Python + C | quick prototypes; heavier dependencies |
| bpftrace | a small DSL (awk-like) | one-liners and ad-hoc tracing |
| cilium/ebpf | Go | embedding eBPF in Go services |
| aya | Rust | type-safe kernel + user space in Rust |
Just starting? Use bpftrace to explore, then C + libbpf when you're ready to build something real.
When to reach for eBPF (and when not to)
eBPF shines for observability, networking, security, and performance analysis — anywhere you need kernel-level visibility or control with minimal overhead. Here's where it sits versus the usual approaches:
| Approach | How it sees your system | Trade-off |
|---|---|---|
| Agent-based (e.g. Prometheus exporters) | polls /proc, /sys
|
simple, but constant overhead & limited depth |
| Library-based (e.g. OpenTelemetry SDKs) | you instrument your code | rich, but you must change every app |
| Kernel-based (eBPF) | hooks the kernel directly | deep + low overhead, no app changes — but Linux-only, steeper learning curve |
Don't reach for eBPF when: a simple existing tool already does the job; you're on an old kernel (many features need 5.x+, ring buffer needs 5.8+); or you're not on Linux (it's a Linux kernel technology — on a Mac, you run it inside a Linux VM).
Try it yourself (no compiler needed)
The fastest way to see eBPF work is to trace every program your machine launches — live. On any Linux box, with bpftrace:
sudo apt install -y bpftrace
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
printf("%-6d %-16s %s\n", pid, comm, str(args->filename));
}'
Open a second terminal and run anything (ls, date). It shows up instantly:
3310 bash /usr/bin/ls
3310 bash /usr/bin/date
That's a real eBPF program: attached to a kernel tracepoint, checked by the verifier, streaming events to user space — no kernel patching, no app changes.
Want the grown-up version (C + libbpf, CO-RE, ring buffer, a proper make build)? The full runnable example — hello-ebpf — is in the repo linked below.
On a Mac? eBPF is Linux-only — run the snippet inside a quick Ubuntu VM (Multipass/Lima). The repo README has a one-command setup.
Takeaways
- eBPF = safe, event-driven programs inside the kernel — observe or control the system without patching it or your apps.
- It turns the kernel from something you could only read but couldn't easily change into a platform you can safely extend at runtime — and the verifier is why that's safe in production.
- It's powerful, not free: right problem (deep visibility, low overhead, Linux), right time (modern kernel). Don't use it where a simpler tool will do.
Resources
- My hands-on examples: github.com/hyperredstart/hello-ebpf
- Learning eBPF — Liz Rice (the cartoon is from here)
- BPF Performance Tools — Brendan Gregg
- ebpf.io
中文版 / Chinese version: https://hyperredstart.github.io/posts/what-is-ebpf/
New to eBPF? Tell me the one concept that still feels fuzzy, and I'll cover it.
Top comments (0)