Tutorial  on  LinuxProgramming

Inspecting and Monitoring eBPF Applications

When you start the tutorial, you’ll see a Term 1 terminal and an IDE on the right-hand side. You are logged in as laborant, and the current working directory already contains the ebpf-hello-world folder. Inside, you’ll find the eBPF Hello World labs, implemented with ebpf-go — a Golang eBPF framework developed as part of the Cilium project.

This tutorial serves as the continuation of From Zero to Your First eBPF Program and Storing Data in eBPF: Your First eBPF Map, expanding on the introduced concepts.

In this part, we’ll learn how to use bpftool to inspect eBPF programs and maps in the kernel, and bpftop, a top-like interface for monitoring eBPF program activity in real time.

Inspecting and Monitoring eBPF Applications

Before we can inspect or monitor anything, we first need some eBPF application running. The code for this lab is in ebpf-hello-world/lab3 directory. Open the Term 1 terminal, navigate to this folder, then build and run the eBPF application as you did in the previous tutorials.

Forgot how to do it?

Using go generate you compile the eBPF kernel program (hello.c) into an object file (hello_bpf.o) and generate a Go source file (hello_bpf.go) that embeds the object and provides user space helper functions to work with it.

go generate

Then go build picks up the main.go and hello_bpf.go and builds the final eBPF application binary lab3.

go build

Finally, run the eBPF Application using:

sudo ./lab3

Inspecting eBPF Applications

Inspecting eBPF applications is useful for debugging and validation, since it lets you confirm that your programs and maps are correctly loaded into the kernel, or when and by whom the program was loaded. It also gives you visibility into the internal state of maps, so you can track how map entries change over time - like our exec_count eBPF map in our previous tutorial.

The most widely used tool for this purpose is bpftool, maintained within the upstream Linux kernel.

Since this in a eBPF playground, this tool is already installed. Open the Term 2 tab on the right (click the + at the top), and run:

sudo bpftool --help
Why do you need to run it using `sudo`?

Most bpftool operations interact directly with the kernel. Loading, attaching, or inspecting eBPF programs and maps requires privileged access to kernel resources.

As noted in the first tutorial, these actions are limited to processes with CAP_BPF, CAP_SYS_ADMIN, or other specific capabilities—privileges typically granted only to root or processes started with sudo.

Here are some common use cases of bpftool.

Listing and Inspecting eBPF Programs

You can use bpftool to view detailed information about eBPF programs. This includes attributes such as which user loaded the program, when it was loaded, and whether it is currently attached.

sudo bpftool prog list # Shows all eBPF programs currently loaded into the kernel, regardless of whether they are attached to a hook or not.
...
15: tracepoint  name handle_execve_tp  tag 8236b54ceef5a3ce  gpl
        loaded_at 2025-08-28T07:36:11+0000  uid 0
        xlated 560B  jited 375B  memlock 4096B  map_ids 5,7
        btf_id 6
What is the difference between loaded and attached eBPF program?

An eBPF program is loaded when it has been verified and accepted into the kernel, but it isn’t yet active. We'll talk about the verification process in an upcoming tutorial.

A program becomes active and attached when it is bound to a specific hook (like tracepoint/syscalls/sys_enter_execve in our code example), meaning the kernel will actually run it when that event occurs.

The program in the example output above has been assigned the ID 15. This "identity" is a unique number assigned to each eBPF program when it’s loaded into the kernel.

Knowing the ID, you can ask bpftool to show more information about this program.

sudo bpftool prog show id 15 --pretty
{
    "id": 15, 
    "type": "tracepoint", 
    "name": "handle_execve_tp",
    "tag": "8236b54ceef5a3ce", 
    "gpl_compatible": true,
    "loaded_at": 1756366571,
    "uid": 0,
    "orphaned": false, 
    "bytes_xlated": 560,
    "jited": true,
    "bytes_jited": 375, 
    "bytes_memlock": 4096,
    "map_ids": [5,7],
    "btf_id": 6
}
What are all these output variables?
  • id: Unique ID of the eBPF program.
  • type: Type of the eBPF program.
  • name: Name of the eBPF program, which is the function name from the source code.
  • tag: SHA (Secure Hashing Algorithm) sum of the program’s instructions, which can be used as another identifier for the program. The program ID can change every time you load or unload the program, but the tag will remain the same. In fact, you get the same output for all of the following commands
sudo bpftool prog show id 15
sudo bpftool prog show name handle_execve_tp
sudo bpftool prog show tag 8236b54ceef5a3ce
  • gpl_compatible: Whether the program is defined with a GPL-compatible license, i.e., char _license[] SEC("license") = "GPL"; in our kernel code.
  • loaded_at: Unix timestamp showing when the program was loaded.
  • uid: User that loaded the eBPF program. In this case, it is User ID 0 (which is root).
  • orphaned: Whether the program is loaded in the kernel but no longer has any active attachment to a hook.
  • bytes_xlated: Size of translated eBPF bytecode (e.g., 560 bytes) after verifier checks and possible kernel modifications; pretty low-level but not yet machine code.
  • jited: Whether the eBPF program was JIT-compiled from translated eBPF bytecode into native CPU instructions.
  • bytes_jited: Size of generated machine code after JIT compilation (e.g., 375 bytes).
  • bytes_memlock: Amount of memory reserved (e.g., 4,096 bytes) in RAM that cannot be paged out.
  • map_ids: IDs of eBPF maps referenced by this program.
  • btf_id: ID of the program’s associated BTF (BPF Type Format) information. We'll learn about BTF later.

We can also inspect the eBPF bytecode loaded into the kernel (after it’s been verified and possibly modified).

sudo bpftool prog dump xlated id 15
int handle_execve_tp(struct trace_event_raw_sys_enter * ctx):
; const char *filename = (const char *)ctx->args[0];
   0: (79) r3 = *(u64 *)(r1 +16)
   1: (b7) r1 = 0
; struct path_key key = {};
   2: (7b) *(u64 *)(r10 -8) = r1
   3: (7b) *(u64 *)(r10 -16) = r1
   4: (7b) *(u64 *)(r10 -24) = r1
   5: (7b) *(u64 *)(r10 -32) = r1
   6: (7b) *(u64 *)(r10 -40) = r1
   7: (7b) *(u64 *)(r10 -48) = r1
   8: (7b) *(u64 *)(r10 -56) = r1
...

💡 To understand eBPF bytecode, you need to be familiar with how eBPF uses its registers and with the (unofficial) eBPF instructions set.

Or even look at the Just-in-Time (JIT) compiled machine code produced for the same program:

sudo bpftool prog dump jited id 15
int handle_execve_tp(struct trace_event_raw_sys_enter * ctx):
bpf_prog_8236b54ceef5a3ce_handle_execve_tp:
; const char *filename = (const char *)ctx->args[0];
   0:   nopl   0x0(%rax,%rax,1)
   5:   xchg   %ax,%ax
   7:   push   %rbp
   8:   mov    %rsp,%rbp
   b:   sub    $0x108,%rsp
  12:   mov    0x10(%rdi),%rdx
  16:   xor    %edi,%edi
; struct path_key key = {};
  18:   mov    %rdi,-0x8(%rbp)
  1c:   mov    %rdi,-0x10(%rbp)
  20:   mov    %rdi,-0x18(%rbp)
  24:   mov    %rdi,-0x20(%rbp)
  28:   mov    %rdi,-0x28(%rbp)
  2c:   mov    %rdi,-0x30(%rbp)
  30:   mov    %rdi,-0x38(%rbp)
...

This lets you debug or simply learn how your original C code is transformed first into eBPF instructions and then into native CPU instructions.

Listing and Managing eBPF Maps

With bpftool, you can also list, create, update, and delete eBPF map entries.

sudo bpftool map list # Shows all eBPF Maps loaded into the kernel
...
5: hash  name exec_count  flags 0x0
        key 256B  value 8B  max_entries 16384  memlock 5378048B
        btf_id 4
...

Knowing the ID, you can ask bpftool to dump maps content, using:

sudo bpftool map dump id 5

Or lookup a specific map entry:

keyhex=$(python3 - <<'PY'
s=b"/bin/bash\0".ljust(256, b"\x00")
print(" ".join(f"{b:02x}" for b in s))
PY
)
sudo bpftool map lookup id 5 key hex $keyhex

💡 Since we perform the lookup using the hex value of the key, we need to provide the exact 256-byte key the map expects. In our example, we perform the lookup using the /bin/bash key which is slightly tedious to convert.

Or update a value under a specific key in the map:

keyhex=$(python3 - <<'PY'
s=b"/bin/bash\0".ljust(256, b"\x00")
print(" ".join(f"{b:02x}" for b in s))
PY
)
sudo bpftool map update id 5 key hex $keyhex value hex 2a 00 00 00 00 00 00 00

💡 The map’s value size is 8 bytes (__u64), so we must provide it in little-endian order. In this case, setting the value to 42 is expressed as 2a 00 00 00 00 00 00 00.

Or delete a specific entry:

keyhex=$(python3 - <<'PY'
s=b"/bin/bash\0".ljust(256, b"\x00")
print(" ".join(f"{b:02x}" for b in s))
PY
)
sudo bpftool map delete id 5 key hex $keyhex

In practice, your eBPF applications will do all the updates/lookups to the eBPF map, but while debugging your code - these commands often come quite useful.

Debugging and Tracing

Up to this point, we’ve always printed the eBPF kernel program logs using:

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

But exactly the same, can be achieved using:

sudo bpftool prog trace

There’s NO option in bpftool prog trace to only show logs from one particular eBPF program. So, whichever program calls bpf_printk(), the logs are all combined and interleaved in the output of this command.

Anyways, bpf_printk() should only be utilized during the development. Not only high-frequency events can overwhelm the logs/trace buffer and the output of the mentioned commands is corrupted, but also they can cause significant performance overhead on your eBPF application.

Generating the vmlinux.h File

At a high level, an eBPF program needs access to kernel context and data structures to do anything meaningful.

For example, when tracing the execve system call, we accessed the executable path and its arguments. These are exposed through the trace_event_raw_sys_enter struct — which is defined in vmlinux.h.

This header can be generated from /sys/kernel/btf/vmlinux, a file that contains the kernel’s type information (structs, enums, typedefs, function prototypes, etc.), using:

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

💡 We’ve included this file in the code repository to make the examples feel more plug-and-play. In practice, however, this only makes sense in environments where the kernel version is fixed (e.g., a VM image like this one).

For other cases, the header should be generated at build time, either through the Makefile or a //go:generate directive in user space program.

main.go
//go:generate sh -c "bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h"

Listing Available eBPF Features

If you've followed carefully, we’ve used several eBPF helper functions so far, such as bpf_probe_read_user_str, bpf_map_lookup_elem, and bpf_map_update_elem in our kernel program.

But where can you find the complete list of helpers, and are they all available for every program type?

In fact, you can check the eBPF features supported by your kernel with:

sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
Global memory limit for JIT compiler for unprivileged users is 796917760 bytes
CONFIG_BPF is set to y
CONFIG_BPF_SYSCALL is set to y
CONFIG_HAVE_EBPF_JIT is set to y
CONFIG_BPF_JIT is set to y
...

Scanning eBPF program types...
eBPF program_type socket_filter is available
eBPF program_type kprobe is available
eBPF program_type tracepoint is available
eBPF program_type xdp is available
...

Scanning eBPF map types...
eBPF map_type hash is available
eBPF map_type array is available
eBPF map_type prog_array is available
eBPF map_type perf_event_array is available
...
Scanning eBPF helper functions...
eBPF helpers supported for program type socket_filter:
        - bpf_map_lookup_elem
        - bpf_map_update_elem
        - bpf_map_delete_elem
        - bpf_ktime_get_ns
        - bpf_get_prandom_u32
...

The output is quite long, and we haven’t yet covered most of these eBPF program or helper functions—but you get the idea.

This command is especially useful when developing programs across different distributions or kernel versions, since not every feature is always enabled or available.

For example, if you want to check whether your eBPF program type supports bpf_get_socket_cookie() helper function (returns a unique, stable identifier for a socket, allowing you to correlate packets with the same connection across different hooks), this command will tell you.

Other ways to check available eBPF features

bpftool feature probe kernel is useful, but not always a one-size-fits-all solution. A limitation is that it only reports features of the currently running kernel and there’s no built-in way to check whether a specific helper is supported in another kernel version without running that kernel.

In our experience, bpftool can also fail to determine helper support for certain program types, showing messages like:

eBPF helpers supported for program type tracing: Could not determine which helpers are available

With that in mind, we outlined several alternative approaches in one of our blog posts.

There’s much more to bpftool, but here we’ve highlighted some lesser-known features that will come in handy as you progress through these tutorials.

Monitoring eBPF Applications

When you’re running multiple eBPF programs in the kernel, it’s not always directly obvious what they’re doing or how much impact they’re having on the system. That’s where bpftop (developed by Netflix) comes in—a top-like tool for eBPF that lets you monitor your programs.

Since this in a eBPF playground, this tool is already installed. Run it, using:

sudo bpftop

When you start bpftop it's gonna open a list of all the eBPF programs running in the kernel (first image). Choose your program using ↓ and ↑ and click Enter.

After that, you will see four panels (second image):

  • Top-left (Program Information): Program ID, type, name and user space processes that reference BPF programs (in our case our lab3 binary but might be empty for older kernels).
  • Top-right (Total CPU %): Time-series of CPU usage for the program (moving avg ~0.0008%, max 0.004%).
  • Bottom-left (Events per second): Bursty executions with peaks up to 4 eps and a moving avg of 1.
  • Bottom-right (Avg Runtime in ns): Execution time per run; moving avg ~3367 ns (~3.3 µs), max ~12090 ns, shown as periodic spikes corresponding to occuring events (captured execve() syscalls).

💡 bpftop enables global eBPF runtime stats via BPF_ENABLE_STATS (disabled by default). The per-run monitoring (timestamps, counters/atomics) adds overhead and can hurt throughput/latency—especially at high rates—so it's should be used only for debugging or profiling during development.

Congrats, you've came to the end of this tutorial. 🥳

Level up your Server Side game — Join 14,000 engineers who receive insightful learning materials straight to their inbox