---
title: eBPF Basics
url: https://doc.liz6.com/en/linux-kernel/08-ebpf-and-observability/01-ebpf-basics
locale: en
area: linux-kernel
tags:
- linux-kernel
- ebpf-and-observability
date: 2026-06-30
modified: 2026-07-16
description: eBPF allows user-space programs to inject sandboxed code into the kernel for execution—the verifier statically proves safety, JIT compiles to native instructions, maps share data with user-space, and CO-RE enables "compile once, run anywhere." It is to the kernel what JavaScript is to the browser.
---

# eBPF Basics

> eBPF allows user-space programs to inject sandboxed code into the kernel for execution—the verifier statically proves safety, JIT compiles to native instructions, maps share data with user-space, and CO-RE enables "compile once, run anywhere." It is to the kernel what JavaScript is to the browser.

## Overview

Traditionally, there were only two ways to add custom logic to the kernel: modify the kernel source and recompile (a process taking months), or write a kernel module (where a single null pointer dereference can panic the entire machine). eBPF (extended Berkeley Packet Filter) offers a third path: **submit the program as data to the kernel; the kernel first uses a static verifier to prove it is harmless, then JIT-compiles it to native code and attaches it to a hook for execution**. Just as browsers trust JavaScript from arbitrary web pages, the kernel now trusts code submitted by users—this is why it evolved from the original packet filter (cBPF) into a general-purpose kernel programmability platform.

Version coverage: 3.18 ~ 6.x. Today, it supports four major domains:

- **Observability**: bpftrace, bcc, Cilium Hubble
- **Networking**: XDP, TC BPF, Cilium container networking
- **Security**: BPF LSM, seccomp
- **Performance**: Custom logic on hot paths without recompiling the kernel

## Lifecycle

```mermaid
flowchart LR
    subgraph user["👩‍💻 User Space"]
        C["C Code"] -->|"clang -target bpf -O2"| O["eBPF ELF (.o)"]
        O -->|"libbpf + CO-RE relocation"| L["bpf(BPF_PROG_LOAD)"]
        R["Read maps / ringbuf"]
    end
    subgraph kernel["🛡️ Kernel Space"]
        L --> V{"<b>Verifier</b><br><small>Static proof of safety for all paths</small>"}
        V ==>|"Pass"| J["<b>JIT Compilation</b><br><small>bytecode → native instructions</small>"}
        V -.->|"Reject -EACCES<br>+ verifier log"| C
        J --> A["Attach to hook<br><small>kprobe / tracepoint / XDP…</small>"]
        A -->|"Event triggers execution"| M[("BPF maps")]
    end
    M --> R
    classDef gate fill:#d2992226,stroke:#d29922,stroke-width:2.5px
    classDef fast fill:#4493f81f,stroke:#4493f8,stroke-width:2px
    classDef store fill:#3fb9501f,stroke:#3fb950,stroke-width:2px
    class V gate
    class J,A fast
    class M store
```

The key lies in the order: **verify first, then compile, then attach**. A program cannot reach a hook unless the verifier has already proven it is safe on all execution paths.

## Verifier: The Foundation of Safety

`kernel/bpf/verifier.c`, approximately 16,000 lines, is the most complex verifier in the kernel. It performs symbolic execution on the program, exhaustively checking all possible paths for three categories of properties:

| Category | Specific Constraints |
|---|---|
| Control Flow | No unreachable instructions; no infinite loops (since 5.3, provably terminating bounded loops are allowed); no out-of-bounds jumps; call depth ≤ 32 |
| Data Flow | Tracks the type of each register (`PTR_TO_CTX` / `PTR_TO_MAP_VALUE` / `SCALAR_VALUE`…); tracks the value range of each scalar; enforces null pointer checks; ensures map access key/value sizes match |
| Resources | Instruction count limit of 1 million (since 5.1); stack depth of 512 bytes |

When verification fails, `BPF_PROG_LOAD` returns `-EACCES`, accompanied by an instruction-by-instruction verifier log—understanding this log is a daily part of eBPF development.

### Bound Tracking

The verifier's most ingenious mechanism: maintaining four bounds `[smin, smax, umin, umax]` for each register, **learning conditions along branches**:

```c
if (x < 10) {
    // The verifier deduces x ∈ [0, 9] within this branch
    bpf_map_lookup_elem(&map, &x);   // OK: index proven to be within bounds
}
// On the else path, it deduces x ∈ [10, MAX]
```

Array index bounds, valid map keys, and pointer arithmetic not going out of bounds—all statically proven at load time via this interval deduction, with zero runtime overhead.

## JIT Compilation

After passing verification, BPF bytecode is translated instruction-by-instruction into native machine code (on x86, in `arch/x86/net/bpf_jit_comp.c`), with each BPF instruction corresponding to 1~3 x86 instructions:

```
BPF_ADD  reg, imm  →  add $imm, %reg
BPF_LDX  reg, src  →  mov (%src), %reg
BPF_CALL fn        →  call fn        (tail call optimized to jmp)
BPF_EXIT           →  ret
```

Thus, eBPF is not an "interpreter inside the kernel"—execution performance after JIT is close to hand-written kernel code, which is why it can be placed on hot paths like XDP that process every packet.

## BPF Maps: Data Sharing Between Programs

BPF programs themselves are stateless and event-driven; state is stored in **maps**—kernel-space key-value stores that allow data sharing between BPF programs and between BPF and user-space.

```c
// include/uapi/linux/bpf.h — excerpt of common types
enum bpf_map_type {
    BPF_MAP_TYPE_HASH,          // General hash table (most common)
    BPF_MAP_TYPE_ARRAY,         // Fixed-size array (O(1) lookup, no deletion)
    BPF_MAP_TYPE_PERCPU_HASH,   // Per-CPU copy, lock-free
    BPF_MAP_TYPE_LRU_HASH,      // Evicts using LRU when full
    BPF_MAP_TYPE_RINGBUF,       // Ring buffer, streams events to user-space
    BPF_MAP_TYPE_STACK_TRACE,   // Kernel stack trace storage
    BPF_MAP_TYPE_BLOOM_FILTER,  // Fast "possibly in set" check
};
```

BPF side operations via helpers: `bpf_map_lookup_elem` / `bpf_map_update_elem` / `bpf_map_delete_elem`; user-space reads/writes the same map via the `bpf()` system call or libbpf wrappers.

### Ring Buffer vs Perf Buffer

Streaming kernel events continuously to user-space is the main highway for observability scenarios. Before 5.8, perf buffers were used; since then, ringbuf should be used exclusively:

| | Perf Buffer (Legacy) | Ring Buffer (5.8+) |
|---|---|---|
| Buffer Structure | One per CPU | Shared across all CPUs |
| Ordering | Out-of-order across CPUs | Globally ordered |
| Memory Efficiency | Reserved based on busiest CPU | Shared, saves memory |
| Copying | Two copies | Reserve-commit, mmap zero-copy |
| Record Length | Fixed | Variable |

## CO-RE: Compile Once, Run Everywhere

**Problem**: BPF programs reference kernel structures at compile time, with field offsets hardcoded into the bytecode; if a field moves in the next kernel version, the program reads incorrect memory. Early bcc therefore had to compile on the target machine (dragging in a full LLVM + kernel headers).

**Solution** is split into two parts:

1. **BTF (BPF Type Format)**—type descriptions built into the kernel, embedded in `/sys/kernel/btf/vmlinux`, essentially a "kernel debug info" compressed to a few MB.
2. **libbpf's CO-RE relocation**—when loading `.o`, it reads the current kernel's BTF and dynamically corrects all field offsets in the bytecode to the real offsets of the local machine.

Thus, the same `.o` file can run unmodified on 5.10 and 6.5:

```c
#include <bpf/bpf_core_read.h>
x = BPF_CORE_READ(task, pid);   // Offset resolved at load time according to local kernel
```

## bpftool: Managing BPF Objects

```bash
bpftool prog list                          # List loaded programs
bpftool map dump id <map_id>               # Export map contents
bpftool prog dump xlated id <prog_id>      # View BPF bytecode
bpftool prog dump jited  id <prog_id>      # View JIT-compiled x86 assembly
bpftool prog load myprog.o /sys/fs/bpf/myprog   # Load and pin to bpffs
```

## Security Model

eBPF programs are constrained to a subset that "cannot harm the kernel." They **cannot**:

- Run infinitely (instruction count limit + termination verification)
- Perform pointer access without bounds checking
- Read arbitrary kernel memory directly—must go through helpers (`bpf_probe_read_kernel`)
- Write arbitrary kernel memory directly—can only write to their own maps
- Sleep or call `schedule` (sleepable BPF is a restricted exception)
- Call kernel functions outside the whitelist (helpers/kfuncs)

Privilege levels:

| Capability | What it can do |
|---|---|
| `CAP_BPF` | Load most BPF program types |
| `CAP_SYS_ADMIN` | All types, including tracing |
| No privileges | Only if `kernel.unprivileged_bpf_disabled=0`, and only for limited types like socket filters |

Which hooks a program can attach to, and what context each type receives, can be found in [eBPF Program Types](/linux-kernel/08-ebpf-and-observability/02-ebpf-program-types.md); for practical tracing toolchains, see [Tracing and Debugging](/linux-kernel/08-ebpf-and-observability/03-tracing-and-debugging.md) and [ftrace and bpftrace User Space](/systems-programming/08-performance-and-debugging/03-ftrace-and-bpftrace-user-space-tracing.md).

## References

- **Source Code**: `kernel/bpf/` (verifier, syscall, helpers), `arch/x86/net/bpf_jit_comp.c` (JIT), `tools/lib/bpf/` (libbpf), `tools/bpf/bpftool/`
- **Kernel Documentation**: `Documentation/bpf/` (excellent documentation)
- **Book**: "BPF Performance Tools" (Brendan Gregg)
- **LWN**: "A thorough introduction to eBPF" (lwn.net/Articles/740157/)

*Keywords: eBPF, verifier, JIT, BPF maps, BTF, CO-RE, libbpf, bpftool, ring buffer, CAP_BPF*
