Learn the menu of attachment points — and why choosing the right one is most of the skill.
🎯 Pick the right vantage point to see what your system is really doingeBPF can attach at many layers of the stack. Picking the right hook — a stable tracepoint, a flexible kprobe, a user-space uprobe / USDT, or a timed profile — is most of the craft.
In Lesson 3 you dissected a one-liner: probe { filter { action } }.
The very first word — the probe — answers where do I attach?
That single choice decides what you can see, how stable your tool is, and how much it costs.
Think of your machine as a tall stack. Down at the bottom is hardware; at the top is your application's own code; in between sit kernel functions, the syscall boundary, and shared libraries. eBPF can splice a tiny observer into almost every one of those layers. Each layer has its own probe type — its own kind of doorway.
A tracepoint is a named, deliberately-placed marker that kernel developers baked into the source as a public observation point. Because they are an intentional, maintained interface, they are the preferred place to hook: they tend to survive kernel upgrades, and they come with named arguments you can read by field instead of guessing positions.
tracepoint:category:event # e.g. tracepoint:syscalls:sys_enter_openat
The category groups related events (syscalls,
sched, block …); the
event is the specific point. Inside the action you read
arguments through the args struct by name — like
args->filename — which is exactly why named args make
tracepoints so pleasant.
# Count every syscall, grouped by process name sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
sudo).# Which process opens which file? (named arg: args->filename) sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s -> %s\n", comm, str(args->filename)); }'
sudo).Source: bpftrace language reference — probe types · Brendan Gregg — Linux eBPF Tracing
A kprobe dynamically attaches to the entry of essentially any kernel function — even ones nobody designed to be traced. A kretprobe attaches to its return. This is enormously powerful: if a function exists, you can watch it. The catch is the interface is unstable — kernel function names and arguments are internal and can change between kernel versions, quietly breaking your tool.
kprobe:function # entry of a kernel function, e.g. kprobe:vfs_read kretprobe:function # its return
kprobes don't give you named args. You read raw positional arguments as
arg0, arg1, … argN,
and a kretprobe exposes the return value as retval. You have
to know (or look up) what each position means.
# Time vfs_read latency: pair entry + return, store a histogram sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start, tid); }'
sudo).vfs_read works on most kernels — but a kprobe on
some obscure internal helper may vanish or change shape after an upgrade. Reach for a
kprobe when no tracepoint covers what you need, and expect to revisit it.
Source: bpftrace reference guide · bpftrace one-liner tutorial
So far every probe lived in the kernel. A uprobe jumps the fence:
it attaches to a function inside a user-space binary or shared library —
your own app, libc, libssl, a language
runtime. A uretprobe hooks the return. This is how you trace what an
application does without recompiling or restarting it.
uprobe:/path/to/binary:function # e.g. uprobe:/bin/bash:readline uretprobe:/path/to/binary:function # its return
Note the binary path in the middle — that's how a uprobe differs
from a kprobe. You point it at the file on disk that holds the function. As with
kprobes, arguments are positional (arg0…argN) and the return is
retval.
# Watch every line a user types into bash (reads readline's return) sudo bpftrace -e 'uretprobe:/bin/bash:readline { printf("%s typed: %s\n", comm, str(retval)); }'
sudo).USDT stands for User Statically-Defined Tracing. These are the user-space cousins of tracepoints: stable probe points that an application deliberately publishes in its own code as a supported observation interface. Many runtimes ship them — Node.js, the JVM, Python builds, PostgreSQL, libc — so you can trace high-level events ("a GC ran", "a query started") without depending on fragile internal symbol names.
usdt:/path/to/binary:provider:probe_name # e.g. usdt:/usr/lib/libc.so.6:libc:memory_malloc_retry
Just like a uprobe you give it the binary, but instead of a raw function you name a
provider and a probe name the app chose to expose.
Arguments come in as positional arg0…argN.
# List the USDT probes a binary publishes, then hook one sudo bpftrace -l 'usdt:/usr/lib/libc.so.6:*'
sudo).Source: bpftrace language reference — usdt · Brendan Gregg — static vs dynamic instrumentation
Every probe so far is event-driven: it fires when something happens. The profile and interval probes are different — they fire on a clock, regardless of what the system is doing. That makes them the engine of profiling: instead of catching every event (expensive), you take periodic snapshots and let statistics fill in the picture.
| Probe | Fires | Typical use |
|---|---|---|
profile:hz:99 | 99 times/sec, on every CPU | Sample stacks to see where CPU time goes |
interval:s:1 | once/sec, on one CPU | Print a running total / heartbeat |
# Sample kernel stacks 99x/sec on all CPUs — the seed of a flame graph sudo bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
sudo).# Tally scheduler events, then print + exit every 5 seconds sudo bpftrace -e 'tracepoint:sched:sched* { @[probe] = count(); } interval:s:5 { print(@); clear(@); }'
sudo).Source: bpftrace one-liner tutorial · Brendan Gregg — sampling at 49/99 Hz
The single biggest reason to learn this menu is the split between interfaces that are promised and interfaces that are merely possible. Brendan Gregg's guidance is blunt: use the static probe types wherever you can, because dynamic ones can change from one software version to another and break the tools you build.
No — tracepoints & USDT are static: deliberately placed, maintained, survive upgrades. kprobes & uprobes are dynamic: they reach raw functions whose names/args are internal and can shift between versions.
Often you lose nothing — a tracepoint or USDT probe frequently covers the question with named, documented args. Reach for a kprobe/uprobe only when no stable point exists.
A kprobe/uprobe tool can break silently after a kernel or app upgrade because the underlying symbol moved. Static points are best-effort stable across upgrades.
Source: Brendan Gregg — "try to use the static probe types wherever possible"
One table to keep next to you. Overhead is a rough rule of thumb — actual cost scales with how often the hook fires, not the probe type alone.
| Probe type | What it sees | Stability | Typical overhead | Example probe string |
|---|---|---|---|---|
| tracepoint | Named kernel events (syscalls, sched, block…) with named args | Stable ✓ | Low (per-event) | tracepoint:syscalls:sys_enter_openat |
| kprobe / kretprobe | Entry / return of any kernel function; arg0…N, retval |
Unstable ✗ | Low–med (per-event) | kprobe:vfs_read |
| uprobe / uretprobe | Functions in a user binary / library (your app, libc) | Unstable ✗ | Med (user↔kernel cross) | uprobe:/bin/bash:readline |
| USDT | Probe points an app publishes on purpose (Node, JVM, Postgres…) | Stable ✓ | Low–med (per-event) | usdt:/path/bin:provider:probe |
| profile | Timed CPU samples on every CPU (stacks) | Stable ✓ | Tunable (set the rate) | profile:hz:99 |
| interval | A timer on one CPU — heartbeats, periodic prints | Stable ✓ | Negligible | interval:s:1 |
| software / hardware | Kernel software counters / CPU PMC events (cache misses, cycles) | Stable ✓ | Tunable (count-based) | hardware:cache-misses:1000000 |
bpftrace -lYou don't have to memorize a single probe name. bpftrace -l
lists every probe that matches a pattern, and the pattern supports
glob wildcards (* and ?) exactly like
a real probe string. This is your map of the territory before you ever write an action.
# Every syscall-entry tracepoint sudo bpftrace -l 'tracepoint:syscalls:sys_enter_*' # Anything to do with "openat", across categories sudo bpftrace -l '*openat*' # Add -v to also print a tracepoint's named arguments sudo bpftrace -lv 'tracepoint:syscalls:sys_enter_read'
sudo).-l to find a
probe, -lv to see its args, then write your one-liner. It's
the fastest way to learn what your particular kernel actually exposes.
profile:hz:99 do?args->filename).
Syntax: tracepoint:category:event.arg0…argN), return is retval. The
catch: it's an unstable interface — kernel function names/args are
internal and can change between versions, breaking your tool.uprobe:/path/to/binary:function. Also unstable,
since raw symbol names can change between builds.usdt:/path/to/binary:provider:probe_name.profile:hz:99 samples 99
times/sec on every CPU (great for profiling stacks); interval:s:1
fires once/sec on one CPU for periodic prints.bpftrace -l 'pattern' lists matching probes
(wildcards * / ? supported); add
-v (i.e. -lv) to also show a
tracepoint's named arguments. Workflow: list → inspect → hook.This lesson is a menu, not a memory test. Bring me a real question and we'll pick the hook together. For example: "I want to know which files my Python service opens — tracepoint or uprobe?" or "why would a kprobe-based tool suddenly stop working after I upgraded the kernel?"