eBPF — Coding the kernel without coding the kernel!
eBPF blurs the line between user space and kernel space — safely, surgically, and without a reboot.
The Problem with the Kernel
Kernels are terrifying to modify. You write a kernel module, load it, and suddenly you’re one bad pointer away from a kernel panic. No safety net. No undo. Just a crash and a reboot.
This is why observability, networking, and security tooling at the kernel level has historically been a job for kernel developers only — a tiny, specialised tribe.
eBPF changes that.
Before eBPF:
After eBPF:
So What Is eBPF?
eBPF (extended Berkeley Packet Filter) is a technology baked into the Linux kernel that lets you run sandboxed programs inside the kernel — without modifying kernel source code, loading kernel modules, or rebooting.
Think of it as a tiny programmable VM living in your kernel. You write a program, the kernel’s verifier checks that it’s safe (no infinite loops, no bad memory access), and then it runs — attached to a hook point like a system call, a network event, or a tracepoint.
Your eBPF Program
|
v
[ Verifier ] — rejects unsafe code
|
v
[ JIT Compiler ] — compiles to native machine code
|
v
[ Kernel Hook ] — syscall / kprobe / tracepoint / XDP
The verifier is the magic. It statically analyzes your program before letting it anywhere near the kernel. No memory leaks, no crashes, no kernel panics from your code.
The Python Bridge: BCC
Writing raw eBPF in C is verbose. That’s where BCC (BPF Compiler Collection) comes in. BCC lets you write your eBPF kernel program in C, and control + read it from Python. It’s the fastest way to get something running.
Install it:
sudo apt install bpfcc-tools python3-bpfcc linux-headers-$(uname -r)
Example 1: Hello, Kernel! (syscall tracing)
Let’s start with the simplest possible thing — printing a message every time execve is called (i.e., every time a process is executed).
from bcc import BPF
# The eBPF program - written in C, passed as a string
program = """
int hello(void *ctx) {
bpf_trace_printk("Hello from the kernel!\\n");
return 0;
}
"""
b = BPF(text=program)
# Attach to the execve syscall
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="hello")
# Read output
print("Tracing execve()... Ctrl+C to stop")
b.trace_print()
Run it with sudo python3 hello.py and open another terminal. Every command you run — ls, cat, anything — triggers a print. You're hooking into the kernel in real-time.
Example 2: Who’s Running What? (capturing process names)
Printing “hello” is cute. Let’s be useful — capture which process is calling execve and show its PID and command name.
from bcc import BPF
program = """
#include <uapi/linux/ptrace.h>
int trace_exec(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("PID %d ran: %s\\n", pid, comm);
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="trace_exec")
print(f"{'PID':<8} {'COMM':<20}")
print("-" * 30)
while True:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
print(msg.decode("utf-8", "replace").strip())
except KeyboardInterrupt:
break
Sample output:
Recommended by LinkedIn
PID COMM
------------------------------
PID 4821 ran: bash
PID 4822 ran: ls
PID 4823 ran: python3
Example 3: Latency Heatmap — How Long Are Your Syscalls Taking?
This is where eBPF gets genuinely powerful. We use a BPF histogram to measure read() syscall latency across all processes on your system — with microsecond precision.
from bcc import BPF
from time import sleep
program = """
#include <uapi/linux/ptrace.h>
BPF_HASH(start, u32); // stores entry timestamp per PID
BPF_HISTOGRAM(dist); // histogram of latencies
int syscall__read_entry(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
return 0;
}
int syscall__read_return(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&pid);
if (tsp != 0) {
u64 delta = bpf_ktime_get_ns() - *tsp;
// Store latency in microseconds, log2 bucketed
dist.increment(bpf_log2l(delta / 1000));
start.delete(&pid);
}
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event=b.get_syscall_fnname("read"), fn_name="syscall__read_entry")
b.attach_kretprobe(event=b.get_syscall_fnname("read"), fn_name="syscall__read_return")
print("Tracing read() latency... hit Ctrl+C to dump histogram\n")
try:
sleep(10)
except KeyboardInterrupt:
pass
print("\nread() latency distribution (µs):")
b["dist"].print_log2_hist("µs")
Output looks like this:
read() latency distribution (µs):
µs : count distribution
0 -> 1 : 1823 |████████████████████████████|
2 -> 3 : 447 |██████▉ |
4 -> 7 : 112 |█▊ |
8 -> 15 : 38 |▌ |
16 -> 31 : 9 |▏ |
32 -> 63 : 2 | |
You just built a latency profiler for a kernel syscall. In 30 lines. With no kernel module.
Example 4: TCP Connection Tracker
Now let’s step into networking. This program watches every new outbound TCP connection — like tcptracer or a mini-Wireshark at the kernel level.
from bcc import BPF
import socket
import struct
program = """
#include <net/sock.h>
#include <bcc/proto.h>
BPF_PERF_OUTPUT(events);
struct event_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 dport;
char comm[16];
};
int trace_connect(struct pt_regs *ctx, struct sock *sk) {
struct event_t event = {};
event.pid = bpf_get_current_pid_tgid() >> 32;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.dport = sk->__sk_common.skc_dport;
bpf_get_current_comm(&event.comm, sizeof(event.comm));
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect")
print(f"{'PID':<8} {'COMM':<16} {'SRC':<18} {'DST':<18} {'PORT'}")
print("-" * 68)
def print_event(cpu, data, size):
event = b["events"].event(data)
src = socket.inet_ntoa(struct.pack("I", event.saddr))
dst = socket.inet_ntoa(struct.pack("I", event.daddr))
port = socket.ntohs(event.dport)
comm = event.comm.decode("utf-8", "replace")
print(f"{event.pid:<8} {comm:<16} {src:<18} {dst:<18} {port}")
b["events"].open_perf_buffer(print_event)
print("Tracing TCP connects... Ctrl+C to stop\n")
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
break
This is essentially what tools like Cilium, Falco, and Pixie do under the hood — watching kernel network events in real-time to build service maps, detect anomalies, or enforce policy.
Example 5: Open File Auditor (security use case)
Security teams love eBPF. Let’s build a simple file access auditor — log every file opened by any process.
from bcc import BPF
program = """
#include <uapi/linux/ptrace.h>
int trace_open(struct pt_regs *ctx, const char __user *filename, int flags) {
char fname[256];
u32 pid = bpf_get_current_pid_tgid() >> 32;
char comm[16];
bpf_probe_read_user_str(fname, sizeof(fname), filename);
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("OPEN pid=%-6d comm=%-12s file=%s\\n", pid, comm, fname);
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event=b.get_syscall_fnname("openat"), fn_name="trace_open")
print("Auditing file opens... Ctrl+C to stop\n")
b.trace_print()
Now imagine filtering this output for /etc/passwd, /etc/shadow, or /proc/*/mem. You've just built a lightweight intrusion detection layer — without modifying a single line of the kernel.
The eBPF Ecosystem
Once you’ve played with BCC, you’ll find a rich production-grade ecosystem built on eBPF:
Tool What it does bpftrace One-liners for kernel tracing (like awk for the kernel) Cilium eBPF-native Kubernetes networking & security Falco Runtime security detection using eBPF Pixie Auto-instrumentation for K8s observability Katran Facebook’s eBPF-based L4 load balancer Parca Continuous profiling using eBPF
What Can’t eBPF Do?
eBPF is powerful, but the verifier enforces hard limits:
These constraints are features, not bugs. They’re what make eBPF safe.
Closing Thoughts
eBPF is arguably the most important Linux kernel technology of the last decade. It’s the foundation of modern cloud-native networking (Cilium replaced iptables in most serious Kubernetes deployments), real-time security (Falco, Tetragon), and zero-instrumentation observability.
And the beautiful part? You can start exploring it today, in Python, on any modern Linux machine, without touching a single line of kernel code.
The kernel is no longer a black box. It’s a programmable substrate — and eBPF is your API into it.