PERF-DLFILTER(1)

πŸ‘‘ PERF-DLFILTER(1) β€” the perf manual, but make it cute πŸ’…

okay so this is the page for perf-dlfilter, straight out of the perf manual, and we're gonna get through it together bestie, no cap 🫢


✨ NAME ✨

perf-dlfilter β€” filter sample events using a dynamically loaded shared object file πŸ’–

🌸 SYNOPSIS 🌸

perf script [--dlfilter file.so ] [ --dlarg arg ]...

πŸ¦‹ DESCRIPTION πŸ¦‹

So this option is the whole vibe: it lets you process data through a custom filter that's provided by a dynamically loaded shared object file. You can pass arguments in using --dlarg, and then grab them back with perf_dlfilter_fns.args() ✨

Now, where does it look for your file? If file.so does not contain /, then it gets found either in the current directory, or in the perf tools exec path β€” which is ~/libexec/perf-core/dlfilters for a local build and install (check perf --exec-path, iykyk) β€” or in the dynamic linker paths πŸ”

🧚 API 🧚

the filtering API is literally just this:

#include <perf/perf_dlfilter.h>

struct perf_dlfilter_fns perf_dlfilter_fns;

int start(void **data, void *ctx);
int stop(void *data, void *ctx);
int filter_event(void *data, const struct perf_dlfilter_sample *sample, void *ctx);
int filter_event_early(void *data, const struct perf_dlfilter_sample *sample, void *ctx);
const char *filter_description(const char **long_description);

so, breaking it down πŸŽ€

start β€” if you implement it, it gets called at the beginning, before any calls to filter_event or filter_event_early. Return 0 to say "we good, success", or return a negative error code. You can assign *data and other functions get to use it later. ctx is needed for calls to perf_dlfilter_fns, but heads up: most perf_dlfilter_fns are not valid when called from start πŸ‘€

stop β€” if you implement it, it's called at the end, after any calls to filter_event or filter_event_early. Return 0 to indicate success, or return a negative error code. data is set by start. ctx is needed for calls to perf_dlfilter_fns, but again β€” most perf_dlfilter_fns are not valid when called from stop 🫠

filter_event β€” if you implement it, it gets called for each sample event. Return 0 to keep the sample event, return 1 to filter it out, or return a negative error code. data is set by start. ctx is needed for calls to perf_dlfilter_fns πŸ’«

filter_event_early β€” same as filter_event, except it's called before internal filtering ⚑

filter_description β€” if you implement it, it should return a one-line description of the filter, and optionally a longer description 🌸

🚨 do NOT assume the sample argument is valid (dereferenceable) after filter_event and filter_event_early return.
🚨 and do NOT assume data referenced by pointers in struct perf_dlfilter_sample is valid (dereferenceable) after filter_event and filter_event_early return either. like, respect the lifetime, bestie.

πŸ’œ The perf_dlfilter_sample structure

filter_event and filter_event_early get handed a perf_dlfilter_sample structure, which is giving main character energy and contains these fields:

/*
 * perf sample event information (as per perf script and <linux/perf_event.h>)
 */
struct perf_dlfilter_sample {
        __u32 size; /* Size of this structure (for compatibility checking) */
        __u16 ins_lat;          /* Refer PERF_SAMPLE_WEIGHT_TYPE in <linux/perf_event.h> */
        __u16 p_stage_cyc;      /* Refer PERF_SAMPLE_WEIGHT_TYPE in <linux/perf_event.h> */
        __u64 ip;
        __s32 pid;
        __s32 tid;
        __u64 time;
        __u64 addr;
        __u64 id;
        __u64 stream_id;
        __u64 period;
        __u64 weight;           /* Refer PERF_SAMPLE_WEIGHT_TYPE in <linux/perf_event.h> */
        __u64 transaction;      /* Refer PERF_SAMPLE_TRANSACTION in <linux/perf_event.h> */
        __u64 insn_cnt; /* For instructions-per-cycle (IPC) */
        __u64 cyc_cnt;          /* For instructions-per-cycle (IPC) */
        __s32 cpu;
        __u32 flags;            /* Refer PERF_DLFILTER_FLAG_* above */
        __u64 data_src;         /* Refer PERF_SAMPLE_DATA_SRC in <linux/perf_event.h> */
        __u64 phys_addr;        /* Refer PERF_SAMPLE_PHYS_ADDR in <linux/perf_event.h> */
        __u64 data_page_size;   /* Refer PERF_SAMPLE_DATA_PAGE_SIZE in <linux/perf_event.h> */
        __u64 code_page_size;   /* Refer PERF_SAMPLE_CODE_PAGE_SIZE in <linux/perf_event.h> */
        __u64 cgroup;           /* Refer PERF_SAMPLE_CGROUP in <linux/perf_event.h> */
        __u8  cpumode;          /* Refer CPUMODE_MASK etc in <linux/perf_event.h> */
        __u8  addr_correlates_sym; /* True => resolve_addr() can be called */
        __u16 misc;             /* Refer perf_event_header in <linux/perf_event.h> */
        __u32 raw_size;         /* Refer PERF_SAMPLE_RAW in <linux/perf_event.h> */
        const void *raw_data;   /* Refer PERF_SAMPLE_RAW in <linux/perf_event.h> */
        __u64 brstack_nr;       /* Number of brstack entries */
        const struct perf_branch_entry *brstack; /* Refer <linux/perf_event.h> */
        __u64 raw_callchain_nr; /* Number of raw_callchain entries */
        const __u64 *raw_callchain; /* Refer <linux/perf_event.h> */
        const char *event;
        __s32 machine_pid;
        __s32 vcpu;
};

Note πŸŽ€ machine_pid and vcpu are not original members β€” they were added together later. size is how you figure out at run time whether they're there. PERF_DLFILTER_HAS_MACHINE_PID will be defined if they're present at compile time. Like this:

#include <perf/perf_dlfilter.h>
#include <stddef.h>
#include <stdbool.h>

static inline bool have_machine_pid(const struct perf_dlfilter_sample *sample)
{
#ifdef PERF_DLFILTER_HAS_MACHINE_PID
        return sample->size >= offsetof(struct perf_dlfilter_sample, vcpu) + sizeof(sample->vcpu);
#else
        return false;
#endif
}

πŸ“ The perf_dlfilter_fns structure

The perf_dlfilter_fns structure gets populated with function pointers when the file is loaded. These are the functions filter_event or filter_event_early can call 🌈

struct perf_dlfilter_fns {
        const struct perf_dlfilter_al *(*resolve_ip)(void *ctx);
        const struct perf_dlfilter_al *(*resolve_addr)(void *ctx);
        char **(*args)(void *ctx, int *dlargc);
        __s32 (*resolve_address)(void *ctx, __u64 address, struct perf_dlfilter_al *al);
        const __u8 *(*insn)(void *ctx, __u32 *length);
        const char *(*srcline)(void *ctx, __u32 *line_number);
        struct perf_event_attr *(*attr)(void *ctx);
        __s32 (*object_code)(void *ctx, __u64 ip, void *buf, __u32 len);
        void (*al_cleanup)(void *ctx, struct perf_dlfilter_al *al);
        void *(*reserved[119])(void *);
};

and what each one does πŸ‘‡

  • resolve_ip β€” returns information about ip πŸ’«
  • resolve_addr β€” returns information about addr (if addr_correlates_sym) 🎯
  • args β€” returns arguments from --dlarg options 🧾
  • resolve_address β€” provides information about address. alβ†’size must be set before calling. Returns 0 on success, -1 otherwise. Call al_cleanup() (if present, see below) when al data is no longer needed 🧹
  • insn β€” returns instruction bytes and length πŸ”
  • srcline β€” return source file name and line number πŸ“„
  • attr β€” returns perf_event_attr, refer <linux/perf_event.h> βš™οΈ
  • object_code β€” reads object code and returns the number of bytes read πŸ“¦
  • al_cleanup β€” must be called (if present, so check perf_dlfilter_fns.al_cleanup != NULL) after resolve_address() to free any associated resources 🧼
🚨 do NOT assume pointers obtained via perf_dlfilter_fns are valid (dereferenceable) after filter_event and filter_event_early return. seriously.

πŸ¦‹ The perf_dlfilter_al structure

The perf_dlfilter_al structure contains information about an address:

/*
 * Address location (as per perf script)
 */
struct perf_dlfilter_al {
        __u32 size; /* Size of this structure (for compatibility checking) */
        __u32 symoff;
        const char *sym;
        __u64 addr; /* Mapped address (from dso) */
        __u64 sym_start;
        __u64 sym_end;
        const char *dso;
        __u8  sym_binding; /* STB_LOCAL, STB_GLOBAL or STB_WEAK, refer <elf.h> */
        __u8  is_64_bit; /* Only valid if dso is not NULL */
        __u8  is_kernel_ip; /* True if in kernel space */
        __u32 buildid_size;
        __u8 *buildid;
        /* Below members are only populated by resolve_ip() */
        __u8 filtered; /* true if this sample event will be filtered out */
        const char *comm;
        void *priv; /* Private data. Do not change */
};
🚨 and please, do NOT assume data referenced by pointers in struct perf_dlfilter_al is valid (dereferenceable) after filter_event and filter_event_early return.

🌟 perf_dlfilter_sample flags

The flags member of perf_dlfilter_sample corresponds with the flags field of perf script. The bits of the flags go like this:

/* Definitions for perf_dlfilter_sample flags */
enum {
        PERF_DLFILTER_FLAG_BRANCH       = 1ULL << 0,
        PERF_DLFILTER_FLAG_CALL         = 1ULL << 1,
        PERF_DLFILTER_FLAG_RETURN       = 1ULL << 2,
        PERF_DLFILTER_FLAG_CONDITIONAL  = 1ULL << 3,
        PERF_DLFILTER_FLAG_SYSCALLRET   = 1ULL << 4,
        PERF_DLFILTER_FLAG_ASYNC        = 1ULL << 5,
        PERF_DLFILTER_FLAG_INTERRUPT    = 1ULL << 6,
        PERF_DLFILTER_FLAG_TX_ABORT     = 1ULL << 7,
        PERF_DLFILTER_FLAG_TRACE_BEGIN  = 1ULL << 8,
        PERF_DLFILTER_FLAG_TRACE_END    = 1ULL << 9,
        PERF_DLFILTER_FLAG_IN_TX        = 1ULL << 10,
        PERF_DLFILTER_FLAG_VMENTRY      = 1ULL << 11,
        PERF_DLFILTER_FLAG_VMEXIT       = 1ULL << 12,
};

πŸ”₯ EXAMPLE πŸ”₯

okay so let's filter out everything except branches from "foo" to "bar" β€” iconic behaviour, honestly:

#include <perf/perf_dlfilter.h>
#include <string.h>

struct perf_dlfilter_fns perf_dlfilter_fns;

int filter_event(void *data, const struct perf_dlfilter_sample *sample, void *ctx)
{
        const struct perf_dlfilter_al *al;
        const struct perf_dlfilter_al *addr_al;

        if (!sample->ip || !sample->addr_correlates_sym)
                return 1;

        al = perf_dlfilter_fns.resolve_ip(ctx);
        if (!al || !al->sym || strcmp(al->sym, "foo"))
                return 1;

        addr_al = perf_dlfilter_fns.resolve_addr(ctx);
        if (!addr_al || !addr_al->sym || strcmp(addr_al->sym, "bar"))
                return 1;

        return 0;
}

to build the shared object β€” assuming perf has been installed for the local user, i.e. perf_dlfilter.h is in ~/include/perf β€” you do this πŸ› οΈ

gcc -c -I ~/include -fpic dlfilter-example.c
gcc -shared -o dlfilter-example.so dlfilter-example.o

and to use the filter with perf script:

perf script --dlfilter dlfilter-example.so

πŸ’… NOTES πŸ’…

okay bestie, real talk for a sec β€” because this part is important and I would never let you walk into it unprepared 🫢

your dlfilter .so file is not some self-contained little island. nope. it's dependent on shared libraries, and the way that works is: if those shared libraries change, it may be necessary to rebuild the .so. so like… if your filter is suddenly acting weird after a system update, that's your first suspect, iykyk πŸ”₯

also β€” heads up β€” there may be unexpected results if the .so uses different versions of the shared libraries that perf uses. it's giving "we're wearing the same outfit but it's not the same outfit" energy πŸ’€ nothing technically broken-looking, but nothing quite right either.

the good news? you can check versions super easily. just use the ldd command and go full detective mode πŸ•΅οΈβ€β™€οΈβœ¨


🌈 SEE ALSO 🌈

  • perf-script(1) β€” literally your next read, go click on it, slay πŸ’–

and that's the whole page bestie!! you made it to the end, I'm so proud of you πŸ₯ΉπŸ‘‘

this icon of a manpage is perf, dated 10/31/2026, and it lives at PERF-DLFILTER(1) β€” like a little time capsule of Unix knowledge, and honestly? obsessed. go build something iconic with it πŸ’…βœ¨πŸŽ€

πŸ“¦ where this page comes from