Linux eBPF – The JavaScript of the Linux Kernel

Linux kernel programming is a skill that most developers have either never encountered or are intimidated by. Just thinking about it can be daunting; what if the kernel crashes? The eBPF technology alleviates concerns about kernel programming, providing an impulse to give it a try.

eBPF (extended Berkeley Packet Filter) is a revolutionary technology in the Linux kernel that allows users to run custom code safely and efficiently in the kernel without modifying the kernel source code or loading kernel modules.

Writing eBPF Programs with Python BCC

The best way to learn a new technology is to start with simple examples. First, let’s see how to call eBPF using Python. The Python BCC library lowers the level of writing eBPF programs from “kernel code level” to “Python script level”.

from bcc import BPF
bpf_code = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#define MAX_FILENAME_LEN 256
int trace_open(struct pt_regs *ctx) {
    char comm[TASK_COMM_LEN];
    # Get the name of the currently executing kernel task (process or thread)
    bpf_get_current_comm(&comm, sizeof(comm));
    # bpf_get_current_pid_tgid is used to get the process and thread identifiers that triggered the current eBPF program.
    bpf_trace_printk("process=%s pid=%d\n", comm, bpf_get_current_pid_tgid() >> 32);
    return 0;
}
"""
# Load the eBPF program
b = BPF(text=bpf_code)
# Attach the eBPF program to the kernel function __x64_sys_openat
b.attach_kprobe(event="__x64_sys_openat", fn_name="trace_open")
print("Tracing open() syscalls... Hit Ctrl+C to exit.")
b.trace_print()

The __x64_sys_openat kernel function is a system call for opening or creating directories. Running trace_open.py will show the files currently being opened or created by the Linux system. Here, we use vim to edit a file in one terminal while running trace_open.py in another terminal, and we can see that trace_open.py successfully tracks the vim process. The bpf_trace_printk function outputs the printed logs to the /sys/kernel/debug/tracing/trace_pipe file, and bcc prints the contents of this file, where

  • vim-3382: Process name and PID

  • [008]: CPU core number where the event occurred

  • …21: Scheduling state or priority flag

  • 244.378936: Timestamp (seconds) since system startup

# sudo python3 trace_open.py | grep vim
b''b'             vim-3382    [008] ...21   244.378936: bpf_trace_printk: process=vim pid=3382'
b''b'             vim-3382    [008] ...21   244.378939: bpf_trace_printk: process=vim pid=3382'
b''b'             vim-3382    [008] ...21   244.378941: bpf_trace_printk: process=vim pid=3382'
# ps -ef | grep vim
wq          3382    3308  0 07:49 pts/1    00:00:00 vim
wq          3442    3223  0 07:52 pts/0    00:00:00 grep --color=auto vim
# Using bcc to develop eBPF programs is very convenient. First, write the eBPF function in C, then bcc loads the function and sets the eBPF function mount point.

Writing eBPF Programs in C

Now let’s look at an example of processing packets at the network card driver level: xdp_filter_core.c.

// xdp_filter_core.c
#include <vmlinux.h>           // CO-RE: Use BTF generated vmlinux.h
#include <bpf/bpf_helpers.h>   // BPF helper functions
#include <bpf/bpf_core_read.h> // CO-RE: Include macros like BPF_CORE_READ
#include <bpf/bpf_endian.h>    // Handle byte order conversion
// Ethernet protocol type
#define ETH_P_IP 0x0800  // IPv4 protocol
// Target IP address: 39.156.70.37 (network byte order)
#define TARGET_IP bpf_htonl(0x279C4625)  // 39.156.70.37
// XDP (eXpress Data Path) program entry function,
// XDP is a high-performance network packet processing framework provided by the Linux kernel,
// allowing fast, programmable packet processing using eBPF programs at the network card driver level
SEC("xdp")
int xdp_filter_ip(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;  // Packet end pointer
    void *data = (void *)(long)ctx->data;          // Packet start pointer
    // Check packet length to ensure at least the Ethernet header is present
    if (data + sizeof(struct ethhdr) > data_end) {
        return XDP_DROP;  // Packet too short, drop it
    }
    // Parse Ethernet header
    struct ethhdr *eth = data;
    __be16 proto;
    if (BPF_CORE_READ_INTO(&proto, eth, h_proto) != 0) {
        return XDP_DROP;  // Failed to read protocol, drop it
    }
    if (proto != bpf_htons(ETH_P_IP)) {
        return XDP_PASS;  // Non-IPv4 packet, pass it
    }
    // Parse IP header
    struct iphdr *iph = data + sizeof(struct ethhdr);
    if (iph + 1 > data_end) {  // Ensure IP header is complete
        return XDP_DROP;
    }
    // Read target IP address (CO-RE method)
    __be32 dst_ip;
    if (BPF_CORE_READ_INTO(&dst_ip, iph, daddr) != 0) {
        return XDP_DROP;  // Read failed, drop it
    }
    bpf_printk("Dst IP: %x\n", dst_ip);  // If the target IP is 39.156.70.37, drop the packet
    if (dst_ip == TARGET_IP) {
        return XDP_DROP;
    }
    // Other packets pass
    return XDP_PASS;
}
// Program license
char _license[] SEC("license") = "GPL";

== Compilation ==

Generate vmlinux.h to provide kernel data structures and types, supporting CO-RE compatibility, meaning the program can be compiled once and run on different kernel versions that support BTF without needing to recompile the source code.

sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

Place vmlinux.h in the same directory as xdp_filter_core.c and compile xdp_filter_core.c, where

  • -o2: Enables optimization level 2, balancing code performance and compilation speed to generate efficient eBPF target code.

  • -g: Generates debug information, including symbols and line numbers, facilitating debugging of eBPF programs.

  • -Wall: Enables all common compiler warnings to help identify potential issues in the code.

  • -target bpf: Specifies the target architecture as BPF, indicating clang to generate bytecode suitable for the Linux eBPF virtual machine.

  • -D__TARGET_ARCH_x86: Indicates the target architecture is x86 for conditional compilation of related code.

  • -c: Compiles the source file xdp_filter_core.c into an object file without linking.

# clang -O2 -g -Wall -target bpf -D__TARGET_ARCH_x86 -I. -c xdp_filter_core.c -o xdp_filter_core.o
xdp_filter_core.c:36:17: warning: comparison of distinct pointer types ('struct iphdr *' and 'void *') [-Wcompare-distinct-pointer-types]   36 |     if (iph + 1 > data_end) {  // Ensure IP header is complete      |         ~~~~~~~ ^ ~~~~~~~~
1 warning generated.

== Loading ==

Use the bpftool prog command to load the compiled eBPF object file xdp_filter_core.o into the kernel. The program is stored at the BPF filesystem path /sys/fs/bpf/xdp_filter_core and assigned a unique program ID. At this point, the program is loaded into the kernel but not yet bound to any network card, so it will not process any packets. Where

  • load: Indicates bpftool to load the eBPF program from the target file into the kernel

  • sys/fs/bpf/xdp_filter_core: Specifies the mount path in the BPF filesystem where the eBPF program is stored and managed in the kernel.

# sudo bpftool prog load xdp_filter_core.o /sys/fs/bpf/xdp_filter_core

Use the bpftool net command to attach the loaded XDP program to the specified network card enp0s3 (the author uses a VirtualBox VM, and the network card name is enp0s3). Once attached, the kernel will immediately start calling this XDP program to process all packets on the enp0s3 interface, where

  • attach: Indicates bpftool to attach the eBPF program to the specified network interface or context

  • xdp: Specifies the type of the attached eBPF program as XDP (eXpress Data Path)

  • pinned: Indicates that the target eBPF program is referenced through the path in the BPF filesystem rather than loaded directly from the object file

  • /sys/fs/bpf/xdp_filter_core: Specifies the path of the eBPF program that has been loaded and pinned in the BPF filesystem, pointing to the XDP program to be attached

# sudo bpftool net attach xdp pinned /sys/fs/bpf/xdp_filter_core dev enp0s3

Use the bpftool prog show command to check the loading status of the eBPF program, where

  • 67: This is the unique identifier (ID) of the eBPF program, assigned by the kernel for referencing this program in other bpftool commands

  • xdp_filter_ip: eBPF program name,

  • tag 57cd311f2e27366b: A unique hash value generated based on the eBPF program bytecode

  • gpl: Indicates that this program follows the GNU General Public License (GPL)

  • xlated 16B: Size of the bytecode after kernel verification and translation of the program

  • jited 23B: Size of the JIT compiled code is 23 bytes

  • memlock 4096B: Locks 4096 bytes of memory; eBPF programs need to lock memory to prevent it from being swapped to disk

  • btf_id 82: BTF data ID associated with the program

# sudo bpftool prog show | grep -A 5 xdp
67: xdp  name xdp_filter_ip  tag 57cd311f2e27366b  gpl
loaded_at 2025-09-30T07:55:30+0800  uid 0
xlated 16B  jited 23B  memlock 4096B
btf_id 82
Use the ip link show command to check the interface status, which can also show the eBPF loading status.
# sudo ip link show dev eth0
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 08:00:27:91:8a:e2 brd ff:ff:ff:ff:ff:ff    prog/xdp id 67 name xdp_filter_ip tag 57cd311f2e27366b jited

VirtualBox is not friendly to eBPF XDP programs. On the author’s VirtualBox Ubuntu VM, the eBPF XDP program does not filter packets for 39.156.70.37. The /sys/kernel/debug/tracing/trace_pipe is used to read kernel tracing data in real-time and stream it. By checking this file, the bpf_printk printed kernel debug logs show no output, further confirming that VirtualBox is not friendly to eBPF XDP programs.

# sudo cat /sys/kernel/debug/tracing/trace_pipe

== Unloading ==

Remove the XDP (eXpress Data Path) type eBPF program attached to the device enp0s3 and delete the pinned eBPF object xdp_filter_core from the BPF virtual filesystem.

# sudo bpftool net detach xdp dev enp0s3
# sudo rm /sys/fs/bpf/xdp_filter_core

With the capabilities of eBPF, writing kernel code no longer needs to be approached with trepidation. You can write code using large models and debug it directly on Linux machines.

Leave a Comment