eBPF Course · Lesson 3

Anatomy of a bpftrace One-Liner

Read and write your first real tracing tool by learning the three parts every bpftrace program has.

🎯 Ask the running kernel a precise question — and read the answer
The one idea

Every bpftrace program is probe /filter/ { action } — WHEN something happens, IF a condition holds, DO this.

01 The shape: when / if / do

Last lesson you followed an eBPF program from your one-liner all the way into the kernel and back. Now we slow down and read the one-liner itself. The good news for an app developer: there's almost nothing to memorize. A bpftrace program is always the same tiny shape, repeated.

probe /filter/ { action }
  ▲          ▲           ▲
  WHEN       IF          DO

probe — WHEN

The event you want to hook: a file open, a function call, a timer tick. bpftrace runs your action every time this event fires. Required.

/filter/ — IF

A condition in slashes (the predicate). The action runs only when it's true — e.g. only for one PID, or only for bash. Optional.

{ action } — DO

What to do when the event fires and the filter passes: print something, count it, time it. Lives in curly braces. Required.

💡 If you've written event handlers, you already know this It's the same mental model as button.on("click", /* if valid */ () => { /* do */ }) — except the "events" are things happening deep in the kernel, and you didn't have to add any code to the program being watched.

Source: bpftrace language reference — "each action block consists of three parts: an optional name, an optional predicate, and an action."

02 Dissecting the Lesson 1 one-liner

Here is the exact one-liner from Lesson 1 — the one that prints every file every process opens. Let's pull it apart completely. By the end of this section you'll be able to read it out loud as plain English.

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat' /* no filter */ '{ printf("%s opened %s\n", comm, str(args->filename)); }'
Linux-only — copy and try later on a Linux box (needs sudo). Shown split across the three parts; in real life it's one quoted string.

Said as English: “Every time any process enters the openat syscall, print the process name and the filename it's opening.” That maps exactly onto WHEN / (no IF) / DO. Here is the same program laid out as three labeled zones:

tracepoint:syscalls:sys_enter_openat / (none) / { printf("%s opened %s\n", comm, …) } PROBE — “WHEN” a process enters openat() FILTER — “IF” optional · omitted ACTION — “DO” print the process & the filename comm → process name (a builtin) str(args->filename) → read the filename string from the event's args
One program, three zones. The filter is optional — here it's omitted, so the action runs for every open.

Everything else in this lesson is just zooming into each of those three zones, plus the small vocabulary (comm, args, str()) that lives inside them.

03 The probe — and the type:target name

A probe names the event. Every probe name is a colon-separated string that starts with a probe type (which kind of event) followed by a target (which specific one):

tracepoint:syscalls:sys_enter_openat
    ▲            ▲              ▲
   type      category       the event
 (what kind)         (the specific target)

The first segment — tracepoint here — is the type. It answers “what category of hook is this?” The rest is the target: the exact event of that type. A few you'll meet:

Probe (type:target)Fires when…
tracepoint:syscalls:sys_enter_openatany process calls the openat syscall (entry)
kprobe:vfs_readthe kernel function vfs_read is called
kretprobe:vfs_readvfs_read returns (so you can read retval)
profile:hz:9999 times per second, on every CPU (sampling)
🎯 Why this matters for the mission Choosing the right probe is asking the right question. “Which files are opened?” → a syscall tracepoint. “How long does this kernel function take?” → a kprobe + kretprobe pair. Lesson 4 is the full menu of probe types — this is just a taste so the names stop looking like magic.

Source: bpftrace language reference — probe naming (provider/type then colon-separated target).

04 The filter (predicate) — the “IF”

A probe like tracepoint:syscalls:sys_enter_openat fires for every process on the box — that can be a firehose. The filter (officially the predicate) is a condition wrapped in slashes that runs before the action and decides whether to bother:

tracepoint:syscalls:sys_enter_openat /comm == "bash"/ { printf("%s\n", str(args->filename)); }
                                      ▲ only when this is true does the action run
Linux-only — copy and try later on a Linux box (needs sudo). Prints only the files that bash opens.

The expression inside the slashes is ordinary boolean logic over the builtins you'll meet next. Common shapes:

💡 No filter = match everything The slashes are optional. Omit them and the action runs on every event the probe catches — exactly what the Lesson 1 one-liner did. Add a predicate the moment the output is too noisy.

Source: bpftrace language reference — predicate example /comm == "bash"/.

05 The action block & printf

The action is the code in { … }. For tracing, by far the most common action is printf — and it works just like C's (or Python's f-strings, or Go's fmt.Printf): a format string with % placeholders, followed by the values to fill them in, left to right.

printf("%-6d %-16s opened %s\n", pid, comm, str(args->filename));
         │    │     │            │    │           │
         %d ← pid  │            │    comm        str(...) ← filename
              %s ← (none here; %s slots fill in order)
Linux-only — copy and try later on a Linux box (needs sudo). The full probe/filter were left off for focus — see §02 for the whole program.

The placeholders you'll use 95% of the time:

SpecifierMeansExample value
%da number (integer)1234
%sa stringbash
%-16sstring, left-aligned, padded to 16 wide (tidy columns)bash    
\nnewline — end of the line (not a % placeholder)↵
⚠ Don't forget the \n Without a trailing \n in your format string, every event prints on the same line and the output looks like one giant blob. It's the single most common beginner slip.

Source: bpftrace stdlib — printf "behaves similar to printf() found in C."

06 The builtins you'll use constantly

Inside a filter or action, you don't have to fetch data — bpftrace hands you a set of builtin variables describing “who/what/when” for the event that just fired. These are the words that make one-liners short. Memorize the top handful; the rest you'll look up.

BuiltinWhat it gives you (for the current event)
commname of the running process/thread (e.g. bash, node) — you'll use this most
pidprocess ID of the current thread
tidthread ID of the current thread
uiduser ID of the current thread (e.g. 0 = root)
argsthe traced event's arguments — e.g. args->filename for openat (see §07)
retvalthe value a function returned — only meaningful on kretprobe/return probes
nsecsa timestamp in nanoseconds; subtract two to measure a duration
cpuID of the CPU that's executing right now
kstackthe kernel-side stack trace — “how did we get here, in the kernel?”
ustackthe user-space stack trace — “…in the application?”
curtaskpointer to the kernel's task_struct for the current task (advanced)
💡 comm vs pid pid is the number; comm is the human-readable name (short for “command”). When scanning output you almost always want comm — and often both, so you can grep by name and act on the number.

Source: bpftrace stdlib — builtin variables reference.

07 Functions — printf, str(), and a peek at aggregation

Builtins are nouns; functions are verbs you call inside the action. You've met printf. The other one you'll reach for immediately is str().

str() — turn a pointer into readable text

Many event arguments aren't strings — they're pointers to where a string lives in memory (that's how the kernel passes a filename). str() safely reads the bytes at that address up to the terminating NUL and gives you back actual text:

str(args->filename)   // pointer → "/etc/passwd"
⚠ %s on a raw pointer = garbage If you printf("%s", args->filename) without str(), you're printing a memory address as if it were text. Wrap any string-pointer argument in str().

A peek at count() and hist()

Printing is great for “show me each event.” But for “how often” or “what's the distribution,” you summarize instead with aggregation functions stored in @ maps:

# count opens per process name
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat' '{ @[comm] = count(); }'

# histogram of read() sizes
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read' '{ @bytes = hist(args->ret); }'
Linux-only — copy and try later on a Linux box (needs sudo). bpftrace auto-prints all @ maps when you press Ctrl-C.
🎯 Saving the good part for Lesson 5 @ maps, count(), hist(), and friends are how bpftrace turns a flood of events into a tidy table or histogram with almost no code. That's the whole of Lesson 5 — “Maps & Aggregations.” For now just know these exist and read as “tally this” / “bucket this.”

Source: bpftrace one-liner tutorial — str(), count(), hist().

08 Running it: one-liners, scripts, listing, BEGIN/END

-e one-liners vs .bt script files

Two ways to run the exact same program. For something throwaway, pass it inline with -e. When it grows past a line or two — multiple probes, comments — put it in a .bt file and run that:

# inline, throwaway
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

# saved as opensnoop.bt, then run the file
sudo bpftrace opensnoop.bt
Linux-only — copy and try later on a Linux box (needs sudo). A .bt file is just the program text, no -e and no surrounding quotes.

Finding probes with -l

You rarely know a probe's exact name by heart. -l lists probes, and * is a wildcard — so it doubles as search. This is how you discover what you can hook:

# every syscall-entry tracepoint
sudo bpftrace -l 'tracepoint:syscalls:sys_enter_*'

# anything mentioning "open"
sudo bpftrace -l '*open*'
Linux-only — copy and try later on a Linux box (needs sudo). -l only prints names; it doesn't trace anything.

The special BEGIN and END probes

Two probes don't correspond to a kernel event at all. BEGIN fires once, before tracing starts; END fires once, after you hit Ctrl-C. Perfect for a header line or a closing summary:

sudo bpftrace -e 'BEGIN { printf("Tracing opens... Ctrl-C to end.\n"); }
tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }
END { printf("done\n"); }'
Linux-only — copy and try later on a Linux box (needs sudo). Three probes in one program — bpftrace runs whichever matches the moment.
💡 Casing note Classic, widely-documented bpftrace uses uppercase BEGIN / END (as in the official tutorial and Brendan Gregg's books). Very recent versions also accept lowercase begin / end; you'll see both in the wild — they mean the same thing.

Source: bpftrace one-liner tutorial — listing with -l, BEGIN { printf(...) }.

09 Check yourself

In kprobe:vfs_read /comm == "bash"/ { printf("%d\n", pid); }, which are the three parts, in order?
What does the builtin comm give you?
Why wrap a filename argument in str(...), as in str(args->filename)?
What does the predicate (filter) /pid == 1234/ do?

10 Flashcards

Q: What are the three parts of every bpftrace program?
A: probe (WHEN — the event), an optional /filter/ a.k.a. predicate (IF — a condition), and the { action } (DO — what to run). Probe and action are required; the filter is optional.
Q: What is comm?
A: A builtin holding the name of the current process/thread (short for "command"), e.g. bash or node. Contrast pid, which is the number.
Q: What is args for?
A: It exposes the traced event's arguments. For a tracepoint you read a field like args->filename (current bpftrace also accepts the dot form args.filename).
Q: When is retval meaningful?
A: On a return probe (e.g. kretprobe) — it's the value the traced function returned. On an entry probe there's no return value yet.
Q: What is a predicate, and how is it written?
A: A condition in slashes placed between the probe and the action, e.g. /comm == "bash"/. The action runs only when it's true. Omit it to match every event.
Q: How do you list/search available probes?
A: bpftrace -l '<pattern>' with * as a wildcard, e.g. bpftrace -l 'tracepoint:syscalls:sys_enter_*'. It only lists names; it doesn't trace.
Q: What do the BEGIN and END probes do?
A: They aren't kernel events. BEGIN runs once before tracing starts (e.g. print a header); END runs once after you stop (e.g. print a summary). Recent versions also accept lowercase begin/end.
👩‍🏫 I'm your teacher — ask me anything

Want to test that you can read one cold? Paste me any bpftrace one-liner and ask "break this into WHEN / IF / DO for me." Or try "write me a one-liner that prints which files only the node process opens" — then we'll check it part by part.