Skip to content

Kprobes, Kretprobes, Fentry & Fexit

Blog series: Part 5 — First steps with libbpf (introduces kprobe-style attachment)
Javadoc: @Kprobe · @Fentry
Source: Kprobe.java · Fentry.java
See also: Tracepoints · Uprobes · Attach Cookies & Multi · BPF Maps

Kprobes let you attach BPF programs to almost any kernel function. They are flexible but depend on kernel internals that can change between versions. For stable interfaces prefer tracepoints; use kprobes when no tracepoint covers the function you need.

Hook types

Annotation When it fires Context type Notes
@Kprobe("fn") Entry of kernel function fn Ptr<pt_regs> Works on any kernel function
@Kretprobe("fn") Return of kernel function fn Ptr<pt_regs> Can read return value
@Fentry("fn") Entry (BTF-based) Function-specific typed args Requires BTF, kernel ≥5.5
@Fexit("fn") Return (BTF-based, with args) Function-specific typed args + retval Requires BTF

Prefer @Fentry / @Fexit over @Kprobe when possible. They are faster (no int3 breakpoint), safer, and receive typed arguments via BTF instead of raw register values.

@Kprobe — entry probe

import me.bechberger.ebpf.annotations.bpf.BPF;
import me.bechberger.ebpf.annotations.bpf.BPFFunction;
import me.bechberger.ebpf.annotations.Kprobe;
import me.bechberger.ebpf.bpf.BPFProgram;
import me.bechberger.ebpf.type.Ptr;
import static me.bechberger.ebpf.bpf.raw.Lib_1.*;

@BPF(license = "GPL")
public abstract class OpenAtTracer extends BPFProgram {

    @BPFMapDefinition(maxEntries = 256)
    final BPFRingBuffer<OpenEvent> events = BPFRingBuffer.newInstance(OpenEvent.class);

    @Type
    record OpenEvent(int pid, int tgid, long fd, @Size(256) String filename) {}

    @Kprobe("do_sys_openat2")
    @BPFFunction
    public int onOpenAt2(Ptr<PtDefinitions.pt_regs> ctx) {
        Ptr<OpenEvent> e = events.reserve();
        if (e == null) return 0;

        e.val().pid  = BPFJ.currentPid();
        e.val().tgid = BPFJ.currentTgid();

        // First arg (filename) is in PT_REGS_PARM2
        Ptr<Byte> filenamePtr = Ptr.cast(PT_REGS_PARM2_CORE(ctx));
        BPFJ.bpf_probe_read_user_str(
            Ptr.cast(e.val().filename), 256, filenamePtr);

        events.submit(e);
        return 0;
    }
}

Reading arguments from pt_regs

On x86-64, arguments are passed in registers:

Argument Macro Register
1st PT_REGS_PARM1_CORE(ctx) rdi
2nd PT_REGS_PARM2_CORE(ctx) rsi
3rd PT_REGS_PARM3_CORE(ctx) rdx
4th PT_REGS_PARM4_CORE(ctx) rcx
5th PT_REGS_PARM5_CORE(ctx) r8
Return PT_REGS_RC_CORE(ctx) rax (kretprobe only)

Use the _CORE variants which work correctly with CO-RE BPF (kernel ≥5.5).

@Kretprobe — return probe

@Kretprobe("do_sys_openat2")
@BPFFunction
public int onOpenAt2Return(Ptr<PtDefinitions.pt_regs> ctx) {
    long retval = PT_REGS_RC_CORE(ctx);
    if (retval < 0) {
        BPFJ.bpf_trace_printk("openat2 failed: %ld\n", retval);
    }
    return 0;
}

@Fentry — fast entry probe (BTF-based)

@Fentry("do_sys_openat2")
@BPFFunction
public int onFentryOpenAt2(int dfd, Ptr<Byte> filename, Ptr<open_how> how, long usize) {
    // Arguments are typed directly — no pt_regs needed
    int pid = BPFJ.currentPid();
    BPFJ.bpf_trace_printk("openat2 dfd=%d pid=%d\n", dfd, pid);
    return 0;
}

The compiler plugin reads BTF to determine the function signature and generates the correct SEC("fentry/do_sys_openat2") section.

@Fexit — fast exit probe (BTF-based, has return value)

@Fexit("do_sys_openat2")
@BPFFunction
public int onFexitOpenAt2(int dfd, Ptr<Byte> filename, Ptr<open_how> how, long usize, long ret) {
    // Last parameter is the return value
    if (ret >= 0) {
        BPFJ.bpf_trace_printk("opened fd=%ld\n", ret);
    }
    return 0;
}

Auto-attach

public static void main(String[] args) throws Exception {
    try (OpenAtTracer prog = BPFProgram.load(OpenAtTracer.class)) {
        prog.autoAttachPrograms();   // attaches all @Kprobe / @Fentry / etc.
        prog.events.setCallback(e ->
            System.out.printf("pid=%-6d file=%s%n", e.pid, e.filename));
        while (true) {
            prog.consumeAndSleep(100);
        }
    }
}

One-shot kprobes with GlobalVariable gate

To fire a kprobe only once (useful in tests), gate it with a GlobalVariable<Boolean>:

final GlobalVariable<Boolean> armed = new GlobalVariable<>(true);

@Kprobe("do_sys_openat2")
@BPFFunction
public int onOpenAt2(Ptr<PtDefinitions.pt_regs> ctx) {
    if (!armed.get()) return 0;
    armed.set(false);
    // ... capture event ...
    return 0;
}

Set armed.set(true) from Java whenever you want the next probe to fire.

Finding kernel function names

# Search for functions related to "openat"
sudo grep openat /proc/kallsyms | grep " T " | head -20

# Check if a function is kprobe-able
sudo bpftool feature | grep kprobe

Not all kernel functions can be probed — inlined functions, __init functions, and functions on the kprobe blacklist (/sys/kernel/debug/kprobes/blacklist) cannot be used.


Examples


Further reading