bpftool from Python: inventory and audit
bpftool was our ground truth all book — prog show, map dump, link show, struct_ops register. It speaks JSON, so a small Python wrapper turns it into real tools: an inventory of every loaded program, a runtime 'top' of which BPF costs the most CPU, map dumps, attachment audits, and a kernel-feature summary. Several working examples you can run on any box.
Where bpftrace (Chapter 64) asks the kernel questions, bpftool inspects
and manages what’s already loaded — and it was the cross-check in nearly every
chapter: prog show to confirm our program loaded, map dump to verify a
counter, link show for the attachment, struct_ops register and iter pin to
drive features Aya couldn’t yet author. Like bpftrace it speaks JSON (-j),
so the same subprocess-and-parse pattern turns it into proper tools: a host BPF
inventory, a runtime top, map dumps, and an attachment audit. This
chapter is several working examples, not one.
The code is in examples/65-bpftool-python/. ./demo.sh runs the wrapper’s
commands against a live program; the README.md has the details.
What bpftool sees
Every loaded BPF object has a kernel id, and bpftool enumerates them:
prog show, map show, link show, btf show. With -j each returns a JSON
array you can parse directly. A program object carries far more than its name:
{ "id": 10, "type": "xdp", "name": "xdp_drop", "tag": "005a3d…",
"gpl_compatible": true, "run_time_ns": 81632, "run_cnt": 10,
"loaded_at": 1506715860, "bytes_xlated": 528, "jited": true,
"bytes_jited": 370, "bytes_memlock": 4096, "map_ids": [10],
"pids": [{ "pid": 1, "comm": "systemd" }] }
Three fields make the tools below worth building: map_ids ties a program
to the maps it uses; pids (since Linux 5.8) names the processes holding
the program open — who owns it; and run_time_ns/run_cnt give its total
CPU cost. That last pair is zero unless you enable stats, because collecting
them has a small per-run cost: sysctl -w kernel.bpf_stats_enabled=1 turns them
on (Linux 5.1+).
The wrapper: eight working commands
examples/65-bpftool-python/bpftool_tool.py is dependency-free and wraps
bpftool -j behind one helper, then exposes eight commands — each a useful tool
in its own right:
| Command | What it does | Built on |
|---|---|---|
progs |
host BPF inventory: id, type, name, JIT, memlock, maps, and the processes holding each program | prog show |
top |
programs by avg ns/run (run_time_ns/run_cnt) — which BPF costs the most CPU; warns if stats are off (--enable-stats) |
prog show |
maps |
every map: id, type, name, key/value sizes, max entries, memlock | map show |
dump <name\|id> |
a map’s contents as JSON — the map dump cross-check, scripted |
map dump |
links |
links (attachments) and the program each drives (the Chapter 59 view) | link show |
net |
XDP/tc attachments per interface (the Chapter 60 cross-check) | net show |
features |
which program and map types this kernel supports | feature probe |
audit |
every loaded program with its holders and attachments, joined | prog show + link show |
The top and audit rows are the ones worth dwelling on: top answers “is our
probe expensive?” with the kernel’s own numbers, and audit answers “what’s
running and who put it there?” — the questions that matter in production. The
helper at the centre is four lines:
def bpftool(*args):
out = subprocess.run(["bpftool", "-j", *args], capture_output=True, text=True)
if out.returncode != 0:
raise RuntimeError(out.stderr.strip())
return json.loads(out.stdout or "[]")
Everything else is shaping that JSON into a table or a join. The audit command
is the one to study: it builds a prog_id → [link types] map from link show,
then walks prog show printing each program’s holders and attachments — exactly
the kind of cross-object correlation that’s painful by eye and trivial in twenty
lines of Python over JSON.
Why this matters operationally
bpftool is read-mostly truth. When an Aya program misbehaves, progs/maps
confirm what’s actually loaded versus what you think you deployed; dump
verifies a counter independently of your loader’s own reading; top answers
“is our probe expensive?” with the kernel’s own numbers; and audit answers
“what’s running and who put it there?” — the question that matters when you
inherit a host or chase a regression. Wrapping it in Python means these become
repeatable checks you can run across a fleet, diff over time, or wire into CI,
rather than commands you retype and eyeball.
Build, deploy, observe
cd examples/65-bpftool-python && ./demo.sh
So there’s something to inventory, the demo first starts a throwaway bpftrace
probe in the background (a loaded program with a map), then runs progs,
maps, links, audit, and features against it — and top with stats
enabled so you see real run_cnt/ns figures — before cleaning up. It’s a
terminal tool, so no Grafana panel; the output is the inventory.
Cross-check
[vm]$ sudo bpftool -j prog show | jq '.[].name' # the raw inventory the tool parses
[vm]$ sudo bpftool prog show # human form, to compare
[vm]$ sudo sysctl kernel.bpf_stats_enabled # is run_time being collected?
Running raw bpftool -j prog show and seeing the same ids your progs table
lists is the cross-check that the wrapper is a thin, faithful layer over the tool
you already trust.
What you learned
- bpftool enumerates every loaded BPF object (
prog/map/link/btf show) and emits JSON with-j; program objects exposemap_ids, holderpids(5.8+), andrun_time_ns/run_cnt(needskernel.bpf_stats_enabled=1). - A small Python wrapper turns that into tools: an inventory (
progs/maps), a runtimetop, mapdump, attachmentaudit, and a feature summary — the cross-checks of the whole book, made repeatable. - bpftool is read-mostly ground truth: it confirms what’s actually loaded, what it costs, and who owns it — independent of your loader’s own view.
Next, Chapter 66 tours the BCC tools — the ready-made *-snoop/*-latency
utilities — and drives them from Python, closing the field guide.
Verification status: verified — Fedora 44, kernel 7.1.3. Built and run on the lab VM (Fedora 44, kernel 7.1.3-200.fc44): builds, loads, and attaches cleanly and runs without error. Confirmed on this kernel — attach targets and struct offsets can be version-specific.