eBPF — Coding the kernel without coding the kernel!

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:
Article content
After eBPF:
Article content

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.

Article content

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:

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:

  • No unbounded loops  -  the verifier must be able to prove termination
  • Limited stack size  -  512 bytes max per eBPF program
  • No arbitrary memory access  - must use BPF helper functions
  • No blocking calls  - eBPF programs must be non-blocking in most contexts
  • Kernel version matters  -  many features require Linux 5.x+

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.

To view or add a comment, sign in

More articles by Rishub Kumar

  • Building Your Own Kubernetes Cluster

    Although Kubernetes is often encountered in the virtual realm of public cloud computing—where your main interaction…

Others also viewed

Explore content categories