Developer terminal showing eBPF code and kernel tracing output with Linux books on desk
eBPF Tooling
William  

Bpftool: BPF Program Inspection and Debugging Guide

bpftool is the one command-line tool worth learning for inspecting, loading, and debugging BPF programs and maps, and it ships right in the kernel source tree under tools/bpf/bpftool. The catch: it rewards people who read its output against kernel source and punishes people who treat it as a mysterious black box. If a map dump or prog show looks wrong to you, the tool is almost never wrong. You just don't understand your BPF program yet.

Last updated: 2026-07-17

What is bpftool and why does it exist

bpftool is the reference utility for BPF objects, built on the same libbpf library your loader links against. That shared lineage matters. When bpftool lists a program or dumps a map, it is asking the kernel the same questions your own code asks through libbpf, so its answers are ground truth, not a friendly approximation.

Here is what trips people up. They run bpftool prog show, see something they didn't expect, and go hunting Stack Overflow for a fix. The real problem is usually that they never learned to read the output. bpftool tells you the program ID, type, load time, attached BTF, and how many maps it holds. Every one of those fields answers a question you should already be asking.

I reach for bpftool the moment a BPF program does something I can't explain from my source alone. It is the fastest way to see what the kernel actually loaded, which is often not what you thought you compiled.

bpftool versus libbpf versus raw syscalls

Pick the tool by the job, not by habit. For inspection, prototyping, and generating headers, bpftool wins outright. For a production loader that ships and survives reboots, you want libbpf compiled into your binary. Raw bpf() syscalls are for people writing a loader library, and unless that is you, skip them.

ApproachBest forWhat it costs you
bpftool (CLI)Inspection, debugging, header and skeleton generationNot a runtime; it does not stay resident to keep a program attached
libbpf (library)Shipping a loader, CO-RE portability, typed map accessYou write and compile C
Raw bpf() syscallsBuilding your own toolingEverything libbpf already solved, done by hand

My split is simple. I inspect and prototype with the CLI, then ship with the library. If you are building the loader itself, my notes on how to compile libbpf from source cover the toolchain.

How do you install and build bpftool

Match bpftool to your running kernel, or expect confusing behavior. A bpftool built against a newer kernel may reference helpers and map types your current kernel does not have, and then a feature probe lies to you.

On Debian and Ubuntu, install the linux-tools-$(uname -r) package. That pins the tool to the kernel you booted. My longer walkthrough for installing bpftool on Ubuntu handles the package quirks.

If your distro is behind, build from tools/bpf/bpftool in the kernel source that matches uname -r. Run make there, drop the binary in /usr/local/sbin, and you are current. Check bpftool version afterward. That tells you which libbpf it linked and whether BTF and other features compiled in, which explains half the "why does this flag not exist" questions before they start.

How do you list and inspect loaded BPF programs

System administrator hands at keyboard with multi-monitor terminal showing network packet captures and BPF data

Start with bpftool prog show. It lists every loaded program with its ID, type, name, run count, and attached maps. That listing is your index into everything else. Grab an ID from it, then go deeper.

To see the actual instructions, run bpftool prog dump xlated id <ID> for the verifier-processed bytecode, or dump jited for the native code the CPU runs. The xlated view is what the verifier accepted after its rewrites, which is not always what you wrote.

This is where discipline pays off. When the xlated dump does not match your C, the instinct is to assume the tool mangled it. It didn't. The verifier rewrites map accesses, inlines some helpers, and patches CO-RE relocations, and reading that against the kernel's verifier code shows you exactly why. Trust the dump. Question your mental model.

How do you dump and modify BPF maps

Dump a map with bpftool map dump MAP, where MAP is id <ID>, name <name>, or pinned <path>. That syntax comes straight from the tool on Ubuntu 20.04 and holds across recent releases. The output shows every key and value as hex bytes.

Read that encoding correctly instead of pattern-matching on an example command. bpftool prints raw bytes in the kernel's byte order, so a __u32 value of 1 shows up as 01 00 00 00 on a little-endian box. People see that and think the map is corrupt. It isn't. That is just how the bytes sit in memory.

To write a value, use bpftool map update id <ID> key <bytes> value <bytes>. You supply the same byte layout the dump showed you. If your update seems to vanish, check the map type first. A per-CPU map holds one value slot per CPU, so a single update touches one CPU's copy and the dump shows the rest unchanged.

How do you generate a BTF header for the running kernel

Run this one line and you get a vmlinux.h for CO-RE builds:

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

That reads the BTF the kernel exposes at /sys/kernel/btf/vmlinux and emits every kernel type as C declarations. Your BPF program includes that header, so kernel structs resolve at compile time without pulling in kernel headers. The libbpf docs point at this exact command for good reason.

Understand what BTF is doing, or CO-RE stays a black box. BTF is the type information that lets one compiled object read structs correctly on a kernel with a different memory layout. The header is not the point; the relocations libbpf performs against it are. If /sys/kernel/btf/vmlinux does not exist, your kernel was built without CONFIG_DEBUG_INFO_BTF, and no amount of retrying the command will conjure it. The kernel's own BPF Type Format documentation is the primary reference when the layout confuses you.

How do you generate and use a BPF skeleton

Generate a skeleton with bpftool gen skeleton prog.o > prog.skel.h after you compile with clang -target bpf. The skeleton embeds your object bytes into a C header and gives you open, load, attach, and destroy functions, plus typed handles to every map and program. A tiny main.c then loads the whole thing with a few calls.

Know what the generated code does, because that is where failures land. The skeleton is not automatic; it is plain C that calls libbpf's bpf_object__open_mem on the embedded bytes, then wires up your maps and programs by name. When a skeleton build fails, open the generated header and read it. The official reference lives at tools/bpf/bpftool/Documentation/bpftool-gen.rst in the kernel tree.

Most skeleton errors are a name mismatch or a section the loader could not place. The header names every symbol it expects, so a compile error there points straight at the map or program you renamed and forgot about.

How do you probe kernel BPF feature support

Run bpftool feature probe before you blame your program for a load failure. It walks the running kernel and reports which program types, map types, and helper functions are available. That list is the difference between "my code is broken" and "this kernel cannot do what my code needs."

I check this first whenever a program that loaded on one machine refuses to load on another. A missing helper or an unsupported map type produces a verifier rejection that looks like a bug in your code. It isn't. The feature probe tells you the kernel simply lacks the piece you depend on, which sends you to a kernel upgrade instead of an afternoon of rewriting working logic.

Scope it with bpftool feature probe kernel for the kernel side, or add macros to get output you can drop into a build config.

How do you attach XDP and cgroup programs with bpftool

Attach a pinned XDP program to an interface with bpftool net attach xdp pinned /sys/fs/bpf/prog dev eth0. The pinned path points at a program you already pinned to the BPF filesystem, and dev names the interface. That is the syntax the man page gives, and it is the one people mistype most.

To see what is attached, run bpftool net show or scope it to an interface. It lists XDP and tc programs bound to each device, so you can confirm your attach actually took. If net show comes back empty after an attach you thought succeeded, the attach failed silently or hit the wrong interface.

For cgroup programs, use bpftool cgroup attach <cgroup-path> <attach-type> pinned <prog> and bpftool cgroup show <cgroup-path> to list them. Attachment order matters here. Multiple programs on one hook run in a defined order, and getting it wrong produces behavior the tool will show you but never explain. The kernel logs and your own test are what tell you the order is off.

How do you get structured output for scripting

Add -j for JSON or -p for pretty-printed JSON to almost any bpftool command. Then pipe it into jq. Scraping the human-readable table with awk breaks the first time a column shifts, so do not do it.

For example, bpftool prog show -j | jq '.[].id' gives you every program ID as clean numbers, ready for a loop. This is how I build small scripts that check whether a specific program is still loaded or pull a map ID by name. The JSON schema is stable across releases in a way the text layout is not, which is the whole reason to prefer it for automation.

What should you do when bpftool output doesn't match your expectations

Correlate, don't guess. When bpftool shows something you can't explain, line it up against three other sources before you accept any theory: strace on your loader, the kernel ring buffer via dmesg, and the verifier log.

Run your loader under strace -e bpf to watch the exact bpf() syscalls and their return values. An EPERM there is a permissions or capability problem, not a code bug. A negative return with EINVAL usually means the verifier rejected the program, and the verifier's own message tells you which instruction and why. Read that message top to bottom; it names the register and the check that failed.

Meanwhile dmesg -T | tail catches anything the kernel logged during load. The mistake I see most is trusting the first plausible explanation from a search result instead of reading these three outputs. bpftool shows you the state. strace, the logs, and the verifier show you how it got there. You need both to have a fix that is actually reproducible.

FAQ

Do I need root to run bpftool?

For most commands, yes. Listing programs, dumping maps, and attaching to interfaces all touch privileged kernel state, so you run them with sudo or the right capabilities like CAP_BPF and CAP_NET_ADMIN. A bare prog show as a normal user usually returns nothing rather than an error, which fools people into thinking no programs are loaded.

Why does bpftool show no programs even though mine is running?

Because unpinned programs only live as long as the process that holds their file descriptor. If your loader exits and nothing pinned the program, the kernel frees it and prog show correctly shows nothing. Pin the program under /sys/fs/bpf if you need it to outlive the loader, then it appears in the listing after the process is gone.

Where does bpftool pin objects, and how do I clean them up?

Pins live on the BPF filesystem, mounted at /sys/fs/bpf. A pin is just a file there pointing at a kernel object. To remove one, rm the pin path, which drops that reference; the object goes away once its last reference is gone. Treat leftover pins like stale files, because that is exactly what they are.

Can I run bpftool on a Raspberry Pi or other ARM board?

Yes, as long as the kernel has BPF and BTF enabled. The commands are identical; only the build differs, since you want a bpftool that matches the ARM kernel you booted. My guide to running eBPF on a Raspberry Pi covers the kernel config that makes /sys/kernel/btf/vmlinux show up.

What is the difference between dump xlated and dump jited?

The xlated dump is the BPF bytecode after the verifier processed and rewrote it, still in BPF instructions. The jited dump is that bytecode compiled to your CPU's native instructions. Read xlated when you want to see what the verifier accepted and how it patched your code. Reach for jited only when you are chasing a JIT-specific issue, which is rare.