eBPF: The Kernel's Revolutionary Superpower for Observability, Security, and Networking
1. Introduction: Why eBPF Matters
In the modern computing landscape, the ability to safely execute custom code inside the operating system kernel without requiring kernel module modifications or reboots has long been considered an unattainable ideal. eBPF (Extended Berkeley Packet Filter) shatters this limitation, providing a revolutionary sandboxed runtime environment within the Linux kernel that enables programmers to write custom code which is then dynamically loaded and executed at critical points in the kernel's execution path.
What began as a packet filtering mechanism in the early 1990s has evolved into one of the most significant architectural shifts in Linux kernel history. Today, eBPF powers critical infrastructure at companies like Meta, Google, Netflix, Cloudflare, and Datadog — forming the backbone of high-performance networking, granular security enforcement, zero-overload tracing, and deep observability.
2. Historical Evolution: From BSD to the Linux Kernel
eBPF traces its lineage back to the Berkeley Packet Filter (BPF), originally proposed by Steven McCanne and Jacob Van der Griend in 1992 for the BSD operating system. The original BPF was designed for efficient packet capture — replacing user-space filtering (as used by tcpdump) with in-kernel filtering that dramatically reduced the number of packets that needed to be copied to user space.
The Linux kernel first integrated BPF in version 2.5.45 (1998), but it remained a niche networking tool for two decades. The breakthrough came with kernel 3.18 (2014), where Alexei Starovoitov — eBPF's primary architect — transformed the classic BPF instruction set into the extended 64-bit register format that defines modern eBPF. This architectural evolution unlocked the ability to:
- Access kernel data structures safely through BPF helpers
- Persist state across events via eBPF maps (key-value stores co-owned by kernel and user space)
- Attach programs to virtually any kernel function through kprobes, tracepoints, XDP hooks, and more recently fentry/fexit hooks
- Compile code from C/Rust/Go via the LLVM/Clang BPF backend and load it with the unified bpf() syscall
3. The eBPF Virtual Machine Architecture
At the heart of eBPF lies an elegantly simple yet surprisingly powerful register-based virtual machine with the following design principles:
3.1 Register Model
The VM defines 11 general-purpose 64-bit registers named r0 through r10:
- r0: Return value / exit value (written by program before exit)
- r1–r5: Function arguments (scratch registers, caller-saved)
- r6–r9: Callee-saved registers (preserved across
callinstructions) - r10: Read-only frame pointer (points to the bottom of the stack)
3.2 Instruction Encoding
Every BPF instruction is a fixed 64-bit word with an 8-bit opcode, 4-bit destination register field, 4-bit source register field, a 16-bit signed offset, and a 32-bit immediate value. The instruction set includes:
- Arithmetic/Logic:
add, sub, mul, div, mod, and, or, xor, lsh, rsh, neg, mov(with register and immediate variants) - Load/Store:
ldxb, ldxh, ldxw, ldxdw, stxb, stxh, stxw, stxdwfor byte/halfword/word/dword access to stack and packet memory - Branching:
ja, jeq, jne, jgt, jge, jlt, jle, jset, jsgt, jsge, jslt, jsle(jump always, jump if equal/greater/less/set, etc.) - Function Call:
callfor invoking kernel-provided helper functions and tail-calling other BPF programs - Return:
exitterminates the program and writes r0 to the caller
3.3 Execution Model
eBPF programs follow an event-driven execution model. A program is triggered by a specific kernel event (packet arrival, function entry, syscall, etc.), runs to completion within a bounded time limit, and must not block or sleep. This design ensures that eBPF programs cannot destabilize the kernel through infinite loops or deadlocks.
4. The BPF Verifier: Safety Guarantee Through Static Analysis
The BPF verifier is arguably the most critical component of eBPF — it performs rigorous static analysis on every program before allowing it into the kernel. The verifier ensures five fundamental safety properties:
4.1 Termination
The verifier simulates all possible execution paths through the program and confirms that every path terminates. To guarantee this, it:
- Tracks the maximum number of instructions a program may execute (default limit: 1 million instructions)
- Rejects any backward jump that could create a loop (unless the loop body is provably bounded by an integer/fall-through condition)
- Checks that all branch targets fall within valid instruction boundaries
4.2 Memory Safety
Every memory access must be proven safe at load time:
- All pointer arithmetic must be provably within bounds of allocated memory (stack, map value, packet)
- Accesses to kernel structures require the use of __builtin_bpf_probe_read() (or newer bpf_probe_read_kernel()) for safe memory dereference
- NULL pointer dereferences are rejected unless the pointer is first checked for NULL
- The stack has a fixed limit of 512 bytes (expandable to 2MB with tail calls for complex programs)
4.3 Control-flow Integrity
Ensures that execution follows the intended flow by rejecting instruction sequences that could bypass security checks or cause undefined behavior. All call targets must be valid helper addresses or tail-call map indices.
4.4 Register State Tracking
The verifier maintains a precise abstract state for every register at every instruction point. For each register, it tracks:
- Type (scalar, pointer_to_stack, pointer_to_map_value, pointer_to_packet, etc.)
- Value bounds (min/max for scalars, offset range for pointers)
- Whether the register has been initialized
- Whether the register contains a value that needs tracking (e.g., for precise map-key-convergence analysis)
4.5 Type System
The verifier imposes a strict type system:
- Pointers must maintain their type through arithmetic — a
pointer_to_map_valuecannot be silently coerced topointer_to_stack - Scalars (non-pointer, non-map-value references) cannot be dereferenced
- Map references obtained from
bpf_map_lookup_elem() must be checked for NULL before use
5. eBPF Maps: State Persistence Beyond Individual Events
eBPF programs are stateless between invocations — but real-world applications require persistent state. eBPF solves this through maps: key-value data structures shared between kernel-space eBPF programs and user-space applications.
5.1 Core Map Types
| Map Type | Description | Typical Use Case |
|---|---|---|
| BPF_MAP_TYPE_HASH | Hash table with arbitrary keys/values | Flow counters, connection tracking, configuration lookups |
| BPF_MAP_TYPE_ARRAY | Fixed-size array indexed by integer | Per-CPU statistics, global configuration |
| BPF_MAP_TYPE_PERCPU_HASH | Per-instance hash table (one hash per CPU) | High-contention counters without locking |
| BPF_MAP_TYPE_LRU_HASH | Evicts least-recently-used entries when capacity is reached | Connection state caches |
| BPF_MAP_TYPE_RINGBUF | Lock-free circular buffer for stream data | Event streaming to user space (retracing the older perf buffer) |
| BPF_MAP_TYPE_PROG_ARRAY | Array holding other program file descriptors | Tail-call dispatch tables |
| BPF_MAP_TYPE_LPM_TRIE | Longest-prefix-match tree | IP route lookup, CIDR-based policy |
| BPF_MAP_TYPE_QUEUE / STACK | FIFO/LIFO data structures | Event queuing between kernel and user space |
5.2 Map Lifecycle and Permissions
Maps are created by user space via bpf(BPF_MAP_CREATE) and referenced by file descriptors. They support fine-grained permissions through the BPF_F_RDONLY_PROG flag (read-only access from BPF programs) and BPF_F_RDONLY/BPF_F_WRONLY flags controlling user-space access. Maps persist until all references to them are closed, enabling shared state across multiple eBPF programs and multiple user-space processes.
6. Helper Functions: The Bridge to Kernel Capabilities
eBPF programs cannot call arbitrary kernel functions. They access kernel services through helper functions — a stable ABI that allows the kernel's internal API to evolve independently.
6.1 Essential Helpers by Category
Map Operations:
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)— Lookup an element by keylong bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)— Insert or update an elementlong bpf_map_delete_elem(struct bpf_map *map, const void *key)— Remove an element
Memory & Time:
long bpf_probe_read(void *dst, u32 size, const void *src)— Safe memory read (kernel or user)u64 bpf_ktime_get_ns(void)— High-resolution monotonic timestamp in nanosecondsu64 bpf_get_current_pid_tgid(void)— Retrieve current process PID and thread GIDlong bpf_get_current_comm(char *buf, u32 size)— Get current process name
Tracing & Debugging:
long bpf_trace_printk(const char *fmt, u32 fmt_size, ...)— Print to/sys/kernel/debug/tracing/trace_pipevoid bpf_perf_event_output(void *ctx, struct bpf_map *map, u64 flags, void *data, u64 size)— Write to perf ring buffer
System-level:
long bpf_override_return(struct pt_regs *regs, u64 rc)— Override return value (requires CAP_SYS_ADMIN and CONFIG_BPF_KPROBE_OVERRIDE)
7. XDP: eXpress Data Path — High-performance Networking
XDP represents eBPF's most powerful networking capability, enabling programs to execute at the lowest point in the Linux network stack — directly inside the network driver, before packets are even handed to the kernel's networking layer.
7.1 Why XDP Matters
Traditional in-kernel packet processing (iptables, tc) incurs significant per-packet overhead through the networking stack: socket buffer allocation, protocol dispatch, netfilter hooks, and routing decisions. Each packet can consume thousands of CPU cycles. XDP executes programs at the driver level, after the DMA has filled a receive ring but before any kernel allocations — achieving processing rates of 240 million packets per second per core on modern hardware.
7.2 XDP Actions
An XDP program must return one of the following action codes:
- XDP_PASS: Pass the packet to the normal networking stack for standard processing
- XDP_DROP: Discard the packet immediately (ideal for DDoS filtering)
- XDP_TX: Transmit the packet back out the same NIC it arrived on (for inline load balancing)
- XDP_REDIRECT: Forward the packet to another CPU or another NIC via the
cpumapordevmap - XDP_ABORTED: An error occurred — packet is dropped and the event is logged for troubleshooting
7.3 Real-world Deployment: Cloudflare's DDoS Mitigation
Cloudflare deployed XDP at scale to achieve sub-microsecond DDoS packet filtering at 100Gbps line rate. Their approach identifies attack signatures (SYN floods, amplification attacks) at the XDP layer and drops malicious packets before they consume any kernel resources. This technique reduced their DDoS mitigation overhead from seconds to less than 500 nanoseconds per packet.
8. Use Case Taxonomy
8.1 Networking & Load Balancing
- Cilium: eBPF-based CNI providing L3-L7 network policy, load balancing, and observability for Kubernetes clusters. Replaces kube-proxy with eBPF-driven service routing, eliminating iptables overhead entirely.
- Katran (Meta): Layer 4 load balancer handling billions of requests per second, using XDP for consistent-hashing-based flow annealing and DDoS protection.
8.2 Observability & Tracing
- BCC (BPF Compiler Collection): A toolkit of ready-to-use tracing tools —
execsnoop,opensnoop,biosnoop,tcpconnect,tcplife— each using eBPF to expose kernel-level events with zero configuration. - bpftraceA high-level tracing language inspired by awk and DTrace, enabling one-liners like
bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }' - Pixie: Cloud-native observability using eBPF to auto-instrument HTTP/gRPC/MySQL/PostgreSQL/Kafka traffic without code changes or sidecar proxies.
8.3 Security
- Falco (Sysdig): Cloud-native runtime security engine using eBPF to detect suspicious system calls, file access patterns, and network activity — alerting on anomalies like shell injection, container escape, or unauthorized execve calls.
- Tetragon (Cilium): eBPF-based security observability platform that can enforce security policies directly in-kernel — killing processes or blocking file writes based on loader-signature provenance analysis.
8.4 Performance Profiling
- Parca: Continuous profiling using eBPF to sample CPU execution stacks across all processes with less than 1% overhead, enabling complete flame graph reconstruction without application instrumentation.
- eBPF Flame Graphs: Brendan Gregg's technique of overlaying eBPF-captured stack traces onto flame graphs, transforming CPU performance regression analysis from days of manual investigation to seconds of automated insight.
9. Program Types and Hook Points
eBPF programs must declare their program type, which determines which kernel functions they can attach to:
| Type | Function | Primary Hook |
|---|---|---|
| BPF_PROG_TYPE_KPROBE | Dynamic instrumentation | Any kernel function via kprobe/kretprobe |
| BPF_PROG_TYPE_TRACEPOINT | Static instrumentation | Predefined stable kernel tracepoints |
| BPF_PROG_TYPE_XDP | Network processing | Driver-level RX hook (XDP) |
| BPF_PROG_TYPE_SOCKET_FILTER | Socket-level filtering | Incoming packets at socket attachment |
| BPF_PROG_TYPE_CGROUP_SKB | Cgroup-based network filtering | Ingress/egress for cgroup members |
| BPF_PROG_TYPE_CGROUP_SOCK | Socket creation monitoring | Socket creation events within a cgroup |
| BPF_PROG_TYPE_SK_MSG | Socket message redirect | Unknown process ID/address pair message redirection |
| BPF_PROG_TYPE_RAW_TRACEPOINT | Static tracing (no arg parsing) | Tracepoints with raw struct pt_regs context |
| BPF_PROG_TYPE_LSM | Linux Security Module hooks | Security hook points (BPF_LSM) |
| BPF_PROG_TYPE_STRUCT_OPS | Replacing kernel struct operators | Any struct_ops (e.g., TCP congestion control) |
10. Tail Calling: Advanced Composition and Extensibility
Tail calls (bpf_tail_call()) enable one eBPF program to chain-execute another, achieving program complexity beyond the verifier's single-program instruction limit. The semantics are specific:
- The caller sets up a
BPF_MAP_TYPE_PROG_ARRAYas a dispatch table bpf_tail_call(ctx, &prog_array, index)replaces the currently running program entirely — the caller does not return- The callee receives the same context (
ctx) as its first argument - Default nesting limit: 32 levels (configurable via
sysctl net.core.bpf_jit_kallsyms)
Tail calls form the backbone of Cilium's networking datapath — processing XDP, TC (traffic control), cgroup, and socket-level policies as a sequence of individually-verifiable programs.
11. BPF CO-RE: Portable eBPF Across Kernel Versions
Portability has been eBPF's most persistent challenge. Kernel data structures change between versions, making hard-coded field offsets in eBPF programs fragile. BPF CO-RE (Compile Once – Run Everywhere) solves this by leveraging:
- BTF (BPF Type Format): Kernel-embedded type metadata describing every struct, field, union, and enum. Automatically available on modern kernels.
- Clang relocations: The compiler records which kernel fields the program accesses, encoding them as relocations in the ELF object file.
- libbpf relocation logic: At load time, libbpf rewrites program instructions to use the actual field offsets reported by the running kernel — using BTF to resolve struct layouts.
With CO-RE, a single compiled eBPF binary can run across different kernel versions, distributions, and configurations without recompilation. This transforms eBPF from a development-time tool into a production-grade deployment target.
12. Hello World: A Minimal Loadable eBPF Program
The following illustrates the four essential components of a CO-RE eBPF program:
// minimal.bpf.c
#include
#include
#include
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} exec_count SEC(".maps");
SEC("tp/sched/sched_process_exec")
int BPF_PROG(trace_exec, struct task_struct *p)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count, init = 1;
count = bpf_map_lookup_elem(&exec_count, &pid);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
bpf_map_update_elem(&exec_count, &pid, &init, BPF_ANY);
}
return 0;
}
char _license[] SEC("license") = "GNU";
And the corresponding user-space loader:
// minimal.c
#include
#include "minimal.skel.h"
int main(int argc, char **argv)
{
struct minimal_bpf *skel;
int err;
skel = minimal_bpf__open();
if (!skel) { fprintf(stderr, "Failed to open\n"); return 1; }
err = minimal_bpf__load(skel);
if (err) { fprintf(stderr, "Failed to load: %d\n", err); goto cleanup; }
err = minimal_bpf__attach(skel);
if (err) { fprintf(stderr, "Failed to attach: %d\n", err); goto cleanup; }
printf("Successfully! Press Ctrl+C to stop.\n");
while (1) { sleep(1); }
cleanup:
minimal_bpf__destroy(skel);
return err;
}
Build and run:
clang -g -O2 -target bpf -c minimal.bpf.c -o minimal.bpf.o
bpftool gen skeleton minimal.bpf.o > minimal.skel.h
gcc -g -O2 minimal.c -o minimal -lbpf
RUST_BACKTRACE=1 ./minimal
13. Performance Characteristics and Benchmarks
Understanding eBPF performance requires recognizing the trade-space it occupies:
- Execution speed: JIT-compiled eBPF programs run within 5% of native kernel performance. The verifier adds no runtime overhead — it operates purely at load time.
- Verification time: Approximately 1 ms per 100 instructions for typical programs. Complex programs with 5,000+ instructions may take 50–100 ms to verify.
- Map access overhead: Hash map lookups add 20–50 ns per operation. PERCPU variants eliminate cache-line bouncing for high-contention counters.
- Tail call overhead:
- Memory footprint: A loaded program (excluding maps) consumes ~1–2 MB resident memory, independent of complexity.
13.1 Real-world Performance Data
| Benchmark | Measurement | Result |
|---|---|---|
| XDP packet processing (64B packets) | PPS per core (25GbE NIC) | ~240M pps |
| TC (traffic control) classification | PPS per core | ~30M pps |
| Kprobe instrumentation overhead | ns per traced call | ~50–200 ns |
| Verifier time (10K instructions) | Total verification time | ~80–120 ms |
| eBPF map insertion (hash) | Ops/sec per core | ~20M ops/sec |
| bpf() syscall latency | Round-trip time | ~200–500 ns |
14. The eBPF Ecosystem: Key Projects
| Project | Organization | Focus Area |
|---|---|---|
| libbpf | Kernel community | Core library for loading/attaching/managing eBPF programs |
| BCC | IOVisor | Toolkit + Python/Lua bindings for tracing and profiling |
| bpftrace | Brendan Gregg (tracepage) | High-level tracing language (awk/DTrace-like) |
| Cilium | Isovalent | Kubernetes networking, security, observability (CNI replacement) |
| Tetragon | Cilium | Runtime security enforcement and observability |
| Falco | Sysdig | Cloud-native runtime threat detection |
| Katran | Meta | L4 load balancer (XDP-based, billion-PS scale) |
| bpftool | Kernel community | Essential management tool for introspection and debugging |
| xdp-tools | The IOVisor project | Open-source XDP utilities and helper functions |
15. Future Directions: Kernel 6.11+ and Beyond
- BPF Exceptions: A mechanism for eBPF programs to throw exceptions that user-space handlers can catch and process — enabling more dynamic error handling patterns.
- eBPF for Windows: Microsoft is porting the eBPF runtime to Windows (ebpf-for-windows), promising cross-platform eBPF program execution with safety guarantees on both Linux and Windows.
- Signed eBPF Programs: Cryptographic signature verification for loaded programs — critical for multi-tenant environments where kernel-mode execution must be restricted to authorized code.
- struct_ops Expansion: Allowing eBPF programs to replace arbitrary kernel struct function pointers — already used for custom TCP congestion control, scalable to any callback-based kernel subsystem.
- CPU Scheduler IntegrationFollowing the EEVDF merge, eBPF-based scheduling policies (via sched-ext) are being explored for domain-specific workload optimization — HPC, gaming, ML inference, and real-time audio processing.
- Persistent BPF Objects: Token-based lifetime management enabling eBPF programs and maps to persist beyond any single process — critical for container orchestration agents and systemd services.
Conclusion
eBPF represents a paradigm shift in how we think about the boundary between kernel and user space. No longer a rigid wall with syscall gates, it has become a programmable membrane where custom logic can be injected with the safety guarantees of a sandboxed language and the performance of JIT-compiled native code.
Whether you are a networking engineer building the next generation of software-defined load balancers, a security analyst hunting for lateral movement patterns in real time, or a developer tracing elusive latency spikes through ten layers of abstraction — eBPF provides the tools to see, understand, and influence the kernel without ever writing a kernel module.
The future is programmable, and that programmability flows through eBPF.

发表评论 取消回复