eBPF Course · Lesson 4

Where You Can Hook: Probe Types

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 doing
The one idea

eBPF 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.

01 The menu, organized by layer

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.

USER SPACE Your application code (functions, hot loops) Shared libraries & app binary (libc, libssl, USDT points) KERNEL SPACE Syscall interface (open, read, execve …) Kernel tracepoints (named, maintained events) Kernel functions (vfs_read, tcp_sendmsg …) Hardware & PMCs (CPU counters, cache misses) uprobe uprobe USDT tracepoint syscalls:* tracepoint kprobe / kretprobe hardware / software profile / interval timed sampling — fires on a clock, not an event
The stack, bottom to top — and the probe type that attaches at each layer. Solid arrows are event-driven hooks; the clock on the right is timed sampling that can observe any layer.
💡 The whole skill in one line Two questions decide your probe: which layer has the answer (hardware? a kernel function? a syscall? your app?), and do you want a promise or power — a stable point that survives upgrades, or a flexible one that can reach anything but may break.

02 Tracepoints — the stable kernel instrumentation

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.

Syntax

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(); }'
Linux-only — copy and try later on a Linux box (needs 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)); }'
Linux-only — copy and try later on a Linux box (needs sudo).
🎯 Why this matters for you For "what is the system doing" questions — which files, which syscalls, what is scheduling — tracepoints are almost always the right first reach. Stable, named, and broad.

Source: bpftrace language reference — probe types · Brendan Gregg — Linux eBPF Tracing

03 kprobes / kretprobes — instrument any kernel function

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.

Syntax

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); }'
Linux-only — copy and try later on a Linux box (needs sudo).
⚠ Power vs. promise A kprobe on 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

04 uprobes / uretprobes — hook user-space code

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.

Syntax

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)); }'
Linux-only — copy and try later on a Linux box (needs sudo).
💡 The bridge to your world uprobes are where an app developer's instinct pays off — you can finally instrument your code and the libraries it leans on, live, from the outside. But like kprobes, raw function names are an unstable interface (next build, different symbols). That's exactly the gap USDT fills.

Source: bpftrace language reference — uprobe

05 USDT — stable probe points an app exposes on purpose

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.

Syntax

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:*'
Linux-only — copy and try later on a Linux box (needs sudo).
🎯 Best of both worlds USDT gives you user-space visibility (like a uprobe) and a stability promise (like a tracepoint) — because the app author signed up to keep these points working. When a tool ships USDT, prefer it over poking at raw functions.

Source: bpftrace language reference — usdt · Brendan Gregg — static vs dynamic instrumentation

06 profile / interval — timed sampling, not events

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.

ProbeFiresTypical use
profile:hz:9999 times/sec, on every CPUSample stacks to see where CPU time goes
interval:s:1once/sec, on one CPUPrint 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(); }'
Linux-only — copy and try later on a Linux box (needs sudo).
# Tally scheduler events, then print + exit every 5 seconds
sudo bpftrace -e 'tracepoint:sched:sched* { @[probe] = count(); }
  interval:s:5 { print(@); clear(@); }'
Linux-only — copy and try later on a Linux box (needs sudo).
💡 Why 99 and not 100? 99 Hz is deliberately off from round numbers like 100 so your samples don't march in lock-step with timers that tick at exactly 100 Hz — that would bias the results. We'll build full CPU flame graphs on this idea in Lesson 7 (Profiling).

Source: bpftrace one-liner tutorial · Brendan Gregg — sampling at 49/99 Hz

07 Stable vs. unstable — the real fork in the road

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.

"All probes are basically the same."

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.

"Stable means I lose power."

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.

"If it ran today, it'll run forever."

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"

08 The probe-type cheat sheet

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 typeWhat it seesStabilityTypical overheadExample 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

Source: bpftrace language reference — probe types & syntax

09 Discovering probes with bpftrace -l

You 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'
Linux-only — copy and try later on a Linux box (needs sudo).
💡 The workflow List → inspect → hook. Use -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.

Source: bpftrace(8) man page — -l listing & wildcards

10 Check yourself

You want a tool that keeps working across kernel upgrades. Which probe type is the stable choice?
A uprobe attaches to…
What is the core trade-off between a tracepoint and a kprobe?
What does profile:hz:99 do?

11 Flashcards

Q: What is a tracepoint and why is it the preferred hook?
A: A named, deliberately-placed kernel instrumentation point that's part of a maintained interface. Preferred because it's stable across kernel upgrades and exposes named arguments (e.g. args->filename). Syntax: tracepoint:category:event.
Q: What do kprobe / kretprobe attach to, and what's the catch?
A: A kprobe hooks the entry of (almost) any kernel function; a kretprobe hooks its return. Args are positional (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.
Q: What do uprobe / uretprobe attach to?
A: Functions inside a user-space binary or shared library (your app, libc, libssl…) — no recompile or restart needed. Syntax includes the binary path: uprobe:/path/to/binary:function. Also unstable, since raw symbol names can change between builds.
Q: What is USDT and why is it special?
A: User Statically-Defined Tracing — stable probe points an application publishes on purpose (Node, JVM, Postgres, libc). Gives user-space visibility and a stability promise. Syntax: usdt:/path/to/binary:provider:probe_name.
Q: How does a profile probe differ from every other probe?
A: It's timed, not event-driven — it fires on a clock regardless of what's happening. 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.
Q: Which probe types are stable vs. unstable, and why care?
A: Stable: tracepoints, USDT, profile/interval, software/hardware (static / maintained). Unstable: kprobes and uprobes (dynamic — reach raw functions whose names/args can change between versions). Prefer static probes so your tools survive upgrades.
Q: How do you discover which probes exist on a system?
A: 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.
👩‍🏫 I'm your teacher — ask me anything

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?"