Anatomy of eBPF

In the previous article, I kept things light on the technical side, introducing eBPF at a high level.
This time, we’re going to roll up our sleeves and dissect a simple eBPF program — piece by piece — to see how it’s actually structured.

When I was first learning this, I found the details scattered across multiple blog posts, kernel docs, and GitHub issues. So, the goal here is to bring everything together in one place and make it easy to follow.

A Simple Example to Work With

Let’s start with the program:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/in.h>
#include <linux/in6.h>

char LICENSE[] SEC("license") = "GPL";

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32);     
    __type(value, __u64); 
} ssh_ip_map SEC(".maps");


SEC("sk_lookup")
int ssh_func(struct bpf_sk_lookup *ctx) {
    if (&ctx->local_port == __bpf_constant_htons(22)) {
        __u32 ip_val = &ctx->remote_ip4;
        __u64 *val, count = 1;

        val = bpf_map_lookup_elem(&ssh_ip_map, &ip_val);
        if (val) {
            __sync_fetch_and_add(val, 1);
        } else {
            bpf_map_update_elem(&ssh_ip_map, &ip_val, &count, BPF_ANY);
        }
        return SK_PASS;
    }
    return SK_PASS;
}

At first glance, it looks like a regular C program — headers, structs, a function.
But under the hood, there are some very eBPF-specific elements here. Things like __uint, SEC("sk_lookup"), and __bpf_constant_htons aren’t part of standard C — they come from the eBPF world.


Understanding Sections in an eBPF Program

An eBPF program can have multiple sections, and each section essentially hooks into a different point in the kernel.

Think of each section as its own mini eBPF program — but with one huge advantage: all sections in the same file can share the same maps. That’s why you’ll often see a .maps section defined separately, which all the hooked functions can access.

  • Section name → Defines the program type or attachment location.

  • Function name → Acts as the symbol name in the ELF object (your entry point), and can be anything you want.

So in our example:

  • SEC(".maps") → The shared map section.

  • SEC("sk_lookup") → A program that runs when a socket lookup happens.


Context Parameters — Why They’re Different

Here’s the important part: each section type comes with its own context structure. The kernel passes this into your function, and you can only use what’s allowed for that section type.

Some examples:

  • xdp section → Context is always struct xdp_md.

  • kprobe section → Context is struct pt_regs.

  • tc (Traffic Control) section → Context is struct __sk_buff.

Choosing the wrong context structure? That’s a compile-time error waiting to happen.

Here is an overview of different types of sections and their corresponding structures:


How to Find the Right Context Structures

You don’t have to guess — the Linux source gives you everything.

Some key places to look:

  • usr/include/linux/bpf.h — Core eBPF structures.

  • usr/include/linux/ptrace.h — Register structures for probes.

  • usr/include/trace/events/*.h — Tracepoint event structures.

  • usr/include/linux/socket.h — Socket-related structures.

And if you want to dig into the macros (which can reveal more about available context members), the program types documentation is gold.


Choosing the Right Headers

This part depends on what your program is doing. For example:

  • Networking-related programs need <linux/bpf.h> and <bpf/bpf_helpers.h>

  • Packet processing programs often pull in <linux/if_ether.h> or <linux/ip.h> for header parsing.

  • Probes may require specific kernel headers tied to the subsystem you’re attaching to.


Here is a robust list of all the different types of headers that I/we might need to include for the particular kind of application we might build.

Different types of structures are available in each of these headers, which we might use to pass into the context of any function in the section we define. Therefore, it is very important to verify that the headers are included correctly, the sections are accurate, and the context is passed correctly.

In the next part, we’ll go deeper into the maps section itself — how it’s defined, what the __uint and __type macros actually do, and how the kernel enforces type safety here. :)

8
Subscribe to my newsletter

Read articles from vishal manikanta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

vishal manikanta
vishal manikanta

As a technologist passionate about building robust systems, I am deeply engaged with DevOps, cloud-native technologies, and automation. My technical journey is centered on a deep dive into Golang, where I explore everything from concurrency to building system tools. I am also proficient in Python, applying it to machine learning and data science projects. From architecting Kubernetes clusters to exploring cybersecurity principles and the fundamentals of self-improvement, I am a lifelong learner constantly seeking new challenges. This blog is where I document my projects and share insights from the ever-evolving world of technology.