How eBPF Runs Your Code Inside the Linux Kernel
eBPF lets you run safe, user-written programs inside the Linux kernel. This article walks through the full journey: writing, verification, JIT compilation, attaching to events, and sharing data with user space via maps.
The Secret Life of eBPF: How Your Code Runs in the Linux Kernel
Have you ever wondered how network engineers can install firewall rules that inspect every packet, or how security teams can trace system calls without slowing down the server? The magic behind these capabilities is eBPF (extended Berkeley Packet Filter), and it's doing something that sounds almost impossible: running user-written programs safely inside the Linux kernel.
Let me walk you through what actually happens when an eBPF program goes from your terminal into the heart of the operating system.
The Old Way Was Painful
Before eBPF, if you wanted to observe or modify kernel behavior, you had to either: - Write a kernel module (one crash and your whole system goes down) - Use outdated tracing tools that were slow and limited - Accept that you couldn't dynamically change kernel behavior without a reboot
The Linux community needed something better. They needed a way to inject custom logic into the kernel without risking system stability. That's where eBPF enters the picture.
Step 1: Writing Your eBPF Program
When you write an eBPF program, you're not writing regular code that runs in user space. You're writing a special type of program that will execute in kernel space. Here's what a simple eBPF program looks like (don't worry if the syntax looks strange):
int syscall_execve(struct bpf_syscall_ctx *ctx) {
pid_t pid = bpf_get_current_pid_tgid() >> 32;
char comm[16];
bpf_get_current_comm(comm, sizeof(comm));
bpf_printk("PID %d executed: %s", pid, comm);
return 0;
}
This program attaches to the execve system call and logs whenever a new program starts running. Simple, right? But the interesting part is what happens next.
Step 2: The Verifier - Your Code's Gatekeeper
This is where most people get surprised. Before your eBPF program can run, it must pass through the BPF verifier. Think of this as an extremely strict code reviewer that runs automatically.
The verifier does something remarkable: 1. It simulates every possible execution path through your program 2. It checks that your loops are bounded (must exit within a known number of iterations) 3. It verifies you only access valid memory 4. It confirms you're not introducing infinite loops or kernel crashes
If your program has even one potential issue, the verifier rejects it. No warnings, no second chances. Your program just won't load. This strictness is what makes eBPP so safe compared to kernel modules.
Step 3: Just-In-Time Compilation
Once your program passes the verifier, the kernel doesn't just run it as-is. Instead, it uses a Just-In-Time (JIT) compiler to translate your eBPF bytecode into native machine code for your specific CPU architecture.
Whether you're running on x86, ARM, or RISC-V, the JIT compiler generates optimized native instructions. This is why eBPF programs run almost as fast as natively compiled kernel code.
Step 4: Attaching to the Kernel
Your compiled eBPF program can attach to various kernel events: - Network packets arriving at any network interface - System calls being made by any process - Kernel functions being entered or exited - Performance monitoring counters triggering
The attachment is dynamic - you can load and unload programs without restarting anything. When you run tc filter add dev eth0 bpf da obj myprogram.o sec classifier, you're telling the kernel: "Take this eBPF program and run it for every packet on eth0."
Step 5: Data Sharing via Maps
Here's where eBPF really shines. Your kernel code can't print to the console or write to files (security restrictions). Instead, it communicates with user-space programs through BPF maps.
These are key-value data structures shared between kernel space and user space. Your eBPF program writes data into a map, and a user-space process reads it out. It's like a tunnel between two worlds.
A common pattern at PythonSkillset.com involves: - eBPF program collects packet counts or latency measurements - User-space Go or Python program reads these maps every second - Data gets formatted for Prometheus or another monitoring system
Real-World Example: The PythonSkillset Web Server
Let me give you a concrete scenario we've been working with. One of our web servers was experiencing mysterious latency spikes. Instead of guessing, we wrote a small eBPF program that:
- Attached to the
accept()syscall to track new connections - Used a map to store timestamps per connection
- Attached to
close()syscall to calculate connection duration
The program collected millions of data points with zero perceptible overhead. Without eBPF, we would have needed to modify the web server application or use sampling-based tools that missed the infrequent spikes.
Why This Matters
The eBPF approach fundamentally changes what's possible in system programming. For the first time, we have: - Safety: Your program can't crash the kernel - Performance: Near-native execution speed - Dynamic loading: Load and unload without reboots - Observability: See everything happening in the kernel
Getting Started
If you want to try eBPF yourself, start with BCC (BPF Compiler Collection). The learning curve is real - you need to understand kernel internals, but the payoff is enormous. Write a simple program that counts system calls per process and watch your understanding of Linux deepen.
The next time someone mentions eBPF, you'll know the journey your code takes: from user-space source, through the strict verifier, into JIT-compiled machine code, running safely in the kernel, and reporting back through maps. It's one of the most elegant solutions Linux has ever developed.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.