BTF (BPF Type Format)
BTF (BPF Type Format) is a compact metadata format that encodes the type and debug information of a C program — struct layouts, field offsets, function signatures, variable types, and source-line mappings — in a form small enough to embed permanently inside the running kernel. It is the kernel’s answer to the question “how can a kernel describe its own data structures to a program that was compiled somewhere else?” The kernel’s debug information is normally carried in DWARF, the standard but verbose debugging format, which is far too large to ship in a production kernel image; BTF is a deduplicated, purpose-built distillation of that same type information that achieves roughly 100x size reduction (Nakryiko, BPF CO-RE). A kernel built with
CONFIG_DEBUG_INFO_BTF=yexposes its own complete type description at/sys/kernel/btf/vmlinux, and every compiled BPF object carries BTF describing its own maps, global variables, and functions. BTF is the substrate underneath CO-RE portability, typed map pretty-printing, kfunc resolution,struct_ops, and human-readable verifier error messages. This note covers what BTF is — the format, the kinds, the ELF sections, and how the kernel uses it; the practical “generate a header and build against it” workflow lives in its sibling BTF and vmlinux.h.
Mental Model
Think of BTF as a self-describing type dictionary that travels with code. Where DWARF is a sprawling debug database meant for gdb on a developer’s workstation, BTF is a tiny, append-only table of type records optimized for the kernel to load and walk at runtime. Every entry is a struct btf_type plus optional trailing data, and entries reference each other by type ID — a 1-based index into the type table, where ID 0 is reserved to mean void (kernel.org BTF doc). A pointer-to-int is two records: an INT record describing int, and a PTR record whose type field holds the ID of that INT. Walking a chain of IDs reconstructs any C type, however nested.
There are two distinct BTF blobs in play, and conflating them is the most common confusion:
flowchart LR subgraph KERNEL["Running kernel (built with CONFIG_DEBUG_INFO_BTF=y)"] PAHOLE["pahole<br/>(DWARF -> BTF, dedup)"] VMLINUXBTF["vmlinux BTF<br/>/sys/kernel/btf/vmlinux<br/>(every kernel type)"] MODBTF["module BTF (split)<br/>/sys/kernel/btf/<mod><br/>(built on vmlinux base)"] PAHOLE --> VMLINUXBTF VMLINUXBTF -. base .-> MODBTF end subgraph OBJ["Compiled BPF object (foo.bpf.o)"] DOTBTF[".BTF section<br/>(program's own types:<br/>maps, globals, funcs)"] DOTBTFEXT[".BTF.ext section<br/>(func-info, line-info,<br/>CO-RE relocations)"] end DOTBTF -->|"BPF_BTF_LOAD"| LOADER["bpf() syscall"] VMLINUXBTF -->|"read at load time"| LOADER LOADER --> VERIF["Verifier:<br/>type map kv + globals,<br/>nicer errors via line-info"]
The two BTF blobs and where they come from. What it shows: kernel BTF is produced once at kernel build time by pahole converting DWARF to deduplicated BTF, and lives in sysfs; the program’s own BTF is emitted by Clang into the .BTF / .BTF.ext sections of the object file. Both reach the kernel through the bpf() syscall at load time. The insight to take: BTF is not one thing in one place — “kernel BTF” describes the kernel’s types and is consumed by CO-RE to relocate field accesses; “object BTF” describes the program’s own maps and variables and is what lets the verifier type-check them. The format is identical; only the producer and the role differ.
Why BTF Exists
The motivating problem is portability of type knowledge. A BPF program that reads task_struct->pid must know the byte offset of pid within task_struct. That offset differs between kernel versions, between configurations (a CONFIG_ option that adds a field shifts everything after it), and between vendors. Hard-coding the offset means the program breaks the moment it runs on a kernel whose layout differs from the one it was compiled against. The pre-BTF solution (the BCC model) shipped Clang/LLVM plus kernel headers to every target machine and compiled the program on the host, so the compiler could read the local headers and bake in the correct offsets — heavy, slow, and fragile (Nakryiko, BPF CO-RE). BTF makes the kernel itself carry an authoritative, machine-readable description of its own layout, so a program compiled once can be relocated to any target kernel by consulting that kernel’s BTF at load time.
flowchart TD SRC["one C source<br/>BPF_CORE_READ(task, pid)"] --> OBJ["one compiled object<br/>foo.bpf.o"] OBJ --> Q{"where is 'pid'<br/>inside task_struct<br/>on THIS kernel?"} Q -->|"hard-code the offset<br/>(pre-BTF, naive)"| BREAK["reads the wrong field<br/>on any other build"] Q -->|"ship a compiler +<br/>kernel headers, build on<br/>the target (BCC model)"| HEAVY["correct, but needs<br/>LLVM and headers on<br/>every production host"] Q -->|"ask the target kernel<br/>for its own type table"| BTFQ["/sys/kernel/btf/vmlinux"] BTFQ --> FIX["libbpf rewrites the offset<br/>in the instruction at load time"] FIX --> RUN["same object runs on<br/>5.4, 6.1, 6.12, vendor forks"]
Three answers to one question, and why only the third scales. What it shows: a BPF program that reads a kernel struct field needs a byte offset, and that offset is a property of the target kernel’s build, not of the source code. Hard-coding it is wrong the moment the target differs; compiling on the target is correct but drags a compiler toolchain and matching kernel headers onto every production machine; consulting the target’s own BTF is correct and needs nothing on the target but the kernel itself. The insight to take: BTF’s contribution is not a compression trick — it is a relocation of authority. The authoritative statement of “what struct task_struct looks like” moves out of header files on the build host and into the running kernel, which is the only thing that can possibly know. Everything else BTF enables — typed maps, fentry signatures, readable verifier logs — falls out of having that table available at runtime. The load-time rewriting step is CO-RE; the table it reads is BTF.
The reason BTF had to be invented rather than reusing DWARF is purely size. DWARF describes types in exhaustive per-compilation-unit detail; the kernel’s full DWARF runs to over a hundred megabytes. Embedding that in every production kernel is a non-starter. BTF was designed — drawing inspiration from Sun’s Compact C Type Format (CTF) — for simplicity and compactness so it could be baked into the kernel image (Nakryiko, BTF Dedup). The decisive technique is deduplication (covered below): the same struct defined in thousands of compilation units collapses to a single record. On Linux 4.11, BTF type descriptors started at 101.7 MB (3.15 million descriptors converted one-to-one from DWARF) and dedup compressed them to 0.97 MB with 22,898 descriptors — a 104x reduction in size and 137x fewer records (Nakryiko, BTF Dedup). That is what turns “ship the kernel’s types inside the kernel” from impossible into routine; a typical modern vmlinux BTF blob is only a few megabytes.
The Format: Header, Types, Strings
A BTF blob is laid out as a fixed header followed by a type section and a string section. The header is exactly (include/uapi/linux/btf.h, v6.12):
struct btf_header {
__u16 magic; /* 0xeB9F */
__u8 version; /* BTF_VERSION == 1 */
__u8 flags;
__u32 hdr_len;
__u32 type_off; /* offset of type section */
__u32 type_len; /* length of type section */
__u32 str_off; /* offset of string section */
__u32 str_len; /* length of string section */
};The magic value 0xeB9F (verified in the 6.12 header as #define BTF_MAGIC 0xeB9F and #define BTF_VERSION 1) doubles as an endianness detector: a loader that reads the two magic bytes can tell whether the blob was produced on a big- or little-endian machine and byte-swap accordingly. type_off/type_len and str_off/str_len carve the blob into its two payload sections.
One detail the field names hide, and which the header’s own in-tree comment spells out, is the base those offsets are measured from: “All offsets are in bytes relative to the end of this header” (v6.12 btf.h). So type_off is normally 0 — the type section starts immediately after the 24-byte header — and str_off normally equals type_len, the string section starting right where the type section ends. A parser that treats these as absolute file offsets will read 24 bytes off the front of every section. hdr_len exists so the header can grow: a reader skips hdr_len bytes rather than sizeof(struct btf_header), and forward compatibility costs nothing.
The kernel’s own documentation contains a real, decodable BTF blob, emitted by clang -S -g -O2 --target=bpf and printed as assembler directives (v6.12 btf.rst §6). Its first 24 bytes are exactly this header, and they decode as follows:
packet-beta 0-15: "magic = 0xeB9F (.short 60319)" 16-23: "version = 1" 24-31: "flags = 0" 32-63: "hdr_len = 24 (bytes)" 64-95: "type_off = 0 (types start right after the header)" 96-127: "type_len = 220" 128-159: "str_off = 220 (== type_len)" 160-191: "str_len = 122"
struct btf_header at bit accuracy, carrying the real values from the kernel documentation’s own clang -S dump. What it shows: the entire header is 24 bytes / 192 bits — a 2-byte magic, two single-byte fields, and five 32-bit words — after which the blob is just two opaque byte ranges. The insight to take: BTF’s on-disk shape is almost aggressively boring, and that is deliberate. There is no index, no hash table, no compression, no relocation table; the kernel loads the blob, walks the type section once assigning sequential IDs, and is done. Compare DWARF, which needs abbreviation tables, DIE trees, and per-compilation-unit scoping just to be read. The two-byte magic doing double duty as an endianness probe is the only clever thing in the header — read 0xeB9F and the producer matched your byte order, read 0x9FeB and every multi-byte field in the blob needs swapping.
Because the sections are contiguous and the header is fixed, the whole blob has a layout you can hold in your head:
flowchart LR subgraph BLOB["one BTF blob (e.g. /sys/kernel/btf/vmlinux, or an ELF .BTF section)"] direction LR H["btf_header<br/>24 bytes<br/>hdr_len = 24"] T["type section<br/>type_len bytes<br/>btf_type records, back to back,<br/>each optionally followed by<br/>vlen trailing records"] S["string section<br/>str_len bytes<br/>NUL-terminated names,<br/>first byte is always NUL"] H --> T --> S end T -. "every name_off<br/>points here" .-> S T -. "every type/index_type<br/>points to another record<br/>by 1-based ID" .-> T
The whole-blob layout and the two kinds of pointer inside it. What it shows: a BTF blob is a header plus two flat byte ranges. Records in the type section refer outward to the string section by byte offset (name_off), and inward to each other by type ID — a 1-based ordinal assigned by counting records as they are parsed, never stored on disk. The insight to take: type IDs are positional, not explicit. Nothing in a btf_type record says “I am type 47”; you are type 47 because you are the 47th record. That is why the type section must be parsed strictly sequentially and cannot be indexed into without walking it, and it is also why the ID space is capped — BTF_MAX_TYPE is 0x000fffff, so a single blob holds at most 1,048,575 types, with BTF_MAX_NAME_OFFSET of 0x00ffffff capping the string section at 16 MiB and BTF_MAX_VLEN of 0xffff capping any one struct at 65,535 members.
Every type record begins with the same fixed header, struct btf_type (v6.12 header):
struct btf_type {
__u32 name_off; /* offset into the string section */
__u32 info; /* kind, vlen, kind_flag, packed together */
union {
__u32 size; /* for INT, ENUM, STRUCT, UNION, ENUM64, DATASEC, FLOAT */
__u32 type; /* for PTR, TYPEDEF, VOLATILE, CONST, RESTRICT, FUNC, VAR... */
};
};The name_off is an offset into the string section, not an inline string — names are pooled and deduplicated there. The info word packs three fields, decoded by these macros from the 6.12 header:
#define BTF_INFO_KIND(info) (((info) >> 24) & 0x1f) /* which BTF_KIND_* */
#define BTF_INFO_VLEN(info) ((info) & 0xffff) /* member/param/element count */
#define BTF_INFO_KFLAG(info) ((info) >> 31) /* kind-specific flag bit */BTF_INFO_KIND extracts the kind (bits 24-28, masked to 5 bits — enough for the 20 kinds 0-19). BTF_INFO_VLEN extracts the variable length: for a struct it is the member count, for a function prototype the parameter count, telling the loader how many trailing records follow this btf_type. BTF_INFO_KFLAG is a single high bit whose meaning depends on the kind — for STRUCT/UNION it signals that member offsets encode bitfield sizes; for ENUM it signals signedness; for FWD it distinguishes a forward struct from a forward union.
The header comment enumerates the users precisely: kind_flag is “currently used by struct, union, enum, fwd and enum64”, and the BTF_KIND_ENUM documentation pins its meaning there — “info.kind_flag: 0 for unsigned, 1 for signed” (v6.12 btf.rst §2.2.6).
Drawn at bit accuracy, one btf_type record is 12 bytes, and the interesting one is the middle word:
packet-beta 0-31: "name_off — byte offset into the string section (0 = anonymous)" 32-47: "info: vlen (bits 0-15) — member / param / enumerator count" 48-55: "info: unused (bits 16-23)" 56-60: "info: kind (bits 24-28) — one of BTF_KIND_*, 0..19" 61-62: "info: unused (29-30)" 63-63: "kflag (31)" 64-95: "size (INT, ENUM, STRUCT, UNION, DATASEC, ENUM64) OR type (PTR, TYPEDEF, CONST, FUNC, ...)"
struct btf_type — the 12-byte record every BTF type begins with. What it shows: three 32-bit words. The first is a string-table offset for the name; the second packs three unrelated things into one word (a 16-bit count, a 5-bit kind, and a single flag bit, with 10 bits left unused); the third is a union whose meaning is decided by the kind — a byte size for types that have one, or a type ID for types that merely point at another type. The insight to take: the size/type union is the reason you cannot interpret any BTF record without first decoding its kind. Reading word three as a size when the kind is PTR gives you a garbage number that happens to be a valid type ID; the kind field is not metadata about the record, it is the record’s parsing key. Note also that the bit numbering in the header comment is little-endian within the info word — bits 0-15 are the low half — so on disk, on a little-endian machine, vlen occupies the first two bytes of the word and kind sits in the fourth.
The kernel’s own documentation dump makes this concrete. Its second and third type records appear in the assembler output as (v6.12 btf.rst §6):
.long 0 # BTF_KIND_FUNC_PROTO(id = 1)
.long 218103808 # 0xd000000
.long 2
.long 83 # BTF_KIND_INT(id = 2)
.long 16777216 # 0x1000000
.long 4
.long 16777248 # 0x1000020
Walk it word by word. The first record: name_off = 0, so it is anonymous — correct, function prototypes are never named. info = 0xd000000; applying BTF_INFO_KIND(info) = (info >> 24) & 0x1f gives 0x0d = 13 = BTF_KIND_FUNC_PROTO, and BTF_INFO_VLEN(info) = info & 0xffff gives 0, a prototype with zero parameters. The third word, 2, is the union read as type — the return type is type ID 2. The second record: name_off = 83, which the string dump in the same document shows is where "int" lives. info = 0x1000000 decodes to kind 0x01 = BTF_KIND_INT, vlen = 0. The union reads as size = 4 bytes. And because INT is one of the kinds with trailing data, one more __u32 follows — 0x1000020 — which is not a btf_type field at all but the integer encoding word:
packet-beta 0-3: "unused (bits 28-31)" 4-7: "encoding (24-27) = 0x1 = BTF_INT_SIGNED" 8-15: "offset (16-23) = 0 (bit offset within the storage unit)" 16-23: "unused (8-15)" 24-31: "bits (0-7) = 0x20 = 32 significant bits"
The __u32 that trails every BTF_KIND_INT, decoded from the real value 0x01000020. What it shows: three subfields extracted by BTF_INT_ENCODING (bits 24-27), BTF_INT_OFFSET (bits 16-23) and BTF_INT_BITS (bits 0-7); the encoding nibble is a bitmask of BTF_INT_SIGNED (1<<0), BTF_INT_CHAR (1<<1) and BTF_INT_BOOL (1<<2). Here it reads: signed, no bit offset, 32 significant bits — that is, plain int, exactly matching the "int" string the record’s name_off points at. The insight to take: BTF separates storage size from value size. The record’s size = 4 says the type occupies four bytes; the trailing word’s bits = 32 says all thirty-two of them carry the value. For a bitfield those two disagree, and it is BTF_INT_BITS plus BTF_INT_OFFSET — not size — that tell a pretty-printer how many bits to extract and from where. This is the same information a debugger needs, encoded in one word instead of a DWARF attribute list.
Note the layout drawn here is a bit-field diagram: the packet rows are the named subfields of one 32-bit word in the order the extraction macros define them, not four independent bytes on the wire.
The string section is a simple concatenation: the documentation specifies that “the first string in the string section must be a null string. The rest of string table is a concatenation of other null-terminated strings” (kernel.org BTF doc). The mandatory leading \0 means name_off == 0 always denotes an anonymous/unnamed type, which is the natural representation for anonymous structs, unnamed parameters, and void.
The BTF_KIND_* Catalog
The expressive power of BTF is its catalog of kinds — one per category of C type construct. As of Linux 6.12 there are 19 defined kinds (1 through 19; 0 is BTF_KIND_UNKN), verified verbatim from the UAPI header (v6.12):
| Kind | Value | Describes | Trailing data |
|---|---|---|---|
BTF_KIND_INT | 1 | integer (int, char, _Bool, bitfield base) | one __u32 encoding |
BTF_KIND_PTR | 2 | pointer | none (type = pointee ID) |
BTF_KIND_ARRAY | 3 | array | struct btf_array |
BTF_KIND_STRUCT | 4 | struct | vlen × struct btf_member |
BTF_KIND_UNION | 5 | union | vlen × struct btf_member |
BTF_KIND_ENUM | 6 | enum, ≤32-bit values | vlen × struct btf_enum |
BTF_KIND_FWD | 7 | forward declaration | none |
BTF_KIND_TYPEDEF | 8 | typedef alias | none (type = aliased ID) |
BTF_KIND_VOLATILE | 9 | volatile qualifier | none |
BTF_KIND_CONST | 10 | const qualifier | none |
BTF_KIND_RESTRICT | 11 | restrict qualifier | none |
BTF_KIND_FUNC | 12 | a defined subprogram | none (type = its FUNC_PROTO) |
BTF_KIND_FUNC_PROTO | 13 | function signature | vlen × struct btf_param |
BTF_KIND_VAR | 14 | a global/static variable | struct btf_var |
BTF_KIND_DATASEC | 15 | a data section (.data/.bss/.rodata) | vlen × struct btf_var_secinfo |
BTF_KIND_FLOAT | 16 | floating-point (float, double) | none |
BTF_KIND_DECL_TAG | 17 | a declaration tag (__attribute__) | struct btf_decl_tag |
BTF_KIND_TYPE_TAG | 18 | a type tag (e.g. __user, __rcu) | none |
BTF_KIND_ENUM64 | 19 | enum, up to 64-bit values | vlen × struct btf_enum64 |
The header terminates the enum with NR_BTF_KINDS and BTF_KIND_MAX = NR_BTF_KINDS - 1, so BTF_KIND_MAX == 19 in 6.12.
Nineteen kinds is a lot to hold flat, but they fall into six families, and the family tells you most of what you need:
mindmap root((BTF_KIND_ 1..19)) Leaf types INT 1 - trailing encoding word FLOAT 16 - size only ENUM 6 - 32-bit values ENUM64 19 - 64-bit split lo/hi Composite STRUCT 4 - vlen members UNION 5 - vlen members ARRAY 3 - elem, index, count Indirection PTR 2 - type is the pointee FWD 7 - declared, not defined TYPEDEF 8 - an alias Qualifiers CONST 10 VOLATILE 9 RESTRICT 11 Functions FUNC 12 - a subprogram FUNC_PROTO 13 - a signature Data and annotation VAR 14 - a global DATASEC 15 - a section of globals DECL_TAG 17 - tags a declaration TYPE_TAG 18 - tags a pointer type
The nineteen kinds grouped by what they do to the type graph. What it shows: leaf types terminate a chain; composites and indirection kinds extend it; qualifiers wrap it without changing the underlying type; the function pair and the data pair exist because BTF deliberately encodes more than types. The insight to take: the qualifier family is the one that surprises people. const struct sock * is not one record with a flag — it is three records chained by type ID, and any code that wants the underlying struct must loop, skipping CONST, VOLATILE, RESTRICT and TYPEDEF until it reaches something substantive. That “strip the modifiers” walk is why the kernel carries btf_type_skip_modifiers() (declared in include/linux/btf.h, defined in kernel/bpf/btf.c) and calls it from roughly twenty places, plus a narrower btf_type_skip_qualifiers() documented in the source as “Similar to btf_type_skip_modifiers() but does not skip typedefs” — the distinction matters when you want to display a type by its typedef name rather than resolve it. A naive consumer that reads one record and stops will conclude that const int has no size.
Two entries in that table deserve a second look because their common fields are overloaded in ways the names do not suggest. BTF_KIND_FUNC’s vlen is not a count of anything — the documentation states it carries “linkage information (BTF_FUNC_STATIC, BTF_FUNC_GLOBAL or BTF_FUNC_EXTERN)”, of which “only linkage values of BTF_FUNC_STATIC and BTF_FUNC_GLOBAL are supported in the kernel” (v6.12 btf.rst §2.2.12). And BTF_KIND_FUNC is explicitly not a type: the same document warns that “the type section encodes debug info, not just pure types. BTF_KIND_FUNC is not a type, and it represents a defined subprogram.” A FUNC record is an instance — this particular function, at this name — whose type is the FUNC_PROTO its type field points at. That distinction is load-bearing for fentry, as the next-but-one section shows.
Chaining a real declaration makes the graph structure concrete. Here is how const struct sock *sk — an ordinary tracing-program argument — is represented:
flowchart LR P["ID 41: PTR<br/>info.kind = 2<br/>type -> 40"] --> C["ID 40: CONST<br/>info.kind = 10<br/>type -> 39"] C --> S["ID 39: STRUCT 'sock'<br/>info.kind = 4<br/>vlen = member count<br/>size = sizeof(struct sock)"] S -->|"btf_member[0]<br/>name_off -> '__sk_common'<br/>offset = bit 0"| M0["ID 38: STRUCT<br/>'sock_common'"] S -->|"btf_member[n]<br/>name_off -> 'sk_rcvbuf'<br/>offset = bit 3456"| M1["ID 7: INT 'int'<br/>size 4, 32 bits, signed"] V(("ID 0<br/>void")):::void classDef void fill:#eee,stroke:#999,stroke-dasharray:3 3
One C declaration as a chain of BTF records. What it shows: const struct sock * is three linked records, and the struct itself fans out to one btf_member per field, each carrying a name offset, a type ID, and a bit offset. The ID numbers and the member set here are illustrative — actual IDs, counts and offsets depend on the blob and the kernel build — but the record shape and the linkage between records are exact. The insight to take: this is the whole trick behind field-offset relocation. To answer “where is sk_rcvbuf?”, a loader does not need to understand C; it walks to the STRUCT record, scans vlen members comparing name_off strings, and reads that member’s offset. That is a dozen lines of code against a flat array, which is precisely why it is cheap enough to do at every program load. ID 0 is reserved and always means void, so a PTR whose type is 0 is a void * and a FUNC_PROTO whose type is 0 returns nothing.
A few kinds carry concrete trailing structs worth seeing, all quoted from the 6.12 UAPI header. An integer is followed by one __u32 whose subfields are pulled out by BTF_INT_ENCODING (bits 24-27), BTF_INT_OFFSET (bits 16-23), and BTF_INT_BITS (bits 0-7); the encoding flags are BTF_INT_SIGNED (1<<0), BTF_INT_CHAR (1<<1), and BTF_INT_BOOL (1<<2) — that is how BTF distinguishes a signed 32-bit int from an unsigned one and from a _Bool. Arrays, struct/union members, enum values, and function parameters each have their own trailing record:
struct btf_array { __u32 type; __u32 index_type; __u32 nelems; };
struct btf_member { __u32 name_off; __u32 type; __u32 offset; };
struct btf_enum { __u32 name_off; __s32 val; };
struct btf_param { __u32 name_off; __u32 type; };
struct btf_var { __u32 linkage; };
struct btf_var_secinfo { __u32 type; __u32 offset; __u32 size; };
struct btf_decl_tag { __s32 component_idx; };
struct btf_enum64 { __u32 name_off; __u32 val_lo32; __u32 val_hi32; };struct btf_member’s offset field is where the kind_flag matters: with the flag clear it is a plain bit offset; with the flag set it packs a bitfield size into the top 8 bits and the bit offset into the low 24 (BTF_MEMBER_BITFIELD_SIZE / BTF_MEMBER_BIT_OFFSET) — this is exactly the layout knowledge that lets BTF pretty-print a struct with bitfields. struct btf_enum64 splits a 64-bit value into val_lo32/val_hi32 because the older btf_enum only had a 32-bit __s32 val; ENUM64 was added precisely to represent enumerators that don’t fit in 32 bits, which is why it is a separate kind (19) rather than an extension of kind 6.
The newest four kinds — FLOAT (16), DECL_TAG (17), TYPE_TAG (18), ENUM64 (19) — all exist in 6.12 (confirmed against both the UAPI header and the v6.12 HTML doc). DECL_TAG carries a __attribute__((btf_decl_tag("..."))) annotation attached to a declaration; per the 6.12 BTF doc its type “should be struct, union, func, var or typedef,” and its component_idx points at which struct member or function argument the tag applies to (-1 for the declaration as a whole) (v6.12 BTF doc). TYPE_TAG instead attaches a tag to a type; the same doc notes it “is only emitted for pointer types,” forming the chain ptr -> [type_tag]* -> ... -> base_type. Pointer attributes that the verifier must reason about are carried this way — __kptr, for instance, is defined in libbpf’s bpf_helpers.h as __attribute__((btf_type_tag("kptr"))), i.e. a TYPE_TAG, marking a map-stored kernel pointer (docs.ebpf.io __kptr), and __user/__rcu are recorded similarly. The kernel’s own btf_decl_tag documentation does not name which specific C attributes map to which kind, so beyond __kptr the attribute→kind mapping below should be treated as illustrative.
The ELF Sections: .BTF and .BTF.ext
Inside a compiled BPF object file there are two BTF-bearing ELF sections. The .BTF section holds the header + type section + string section described above — the program’s own types. The .BTF.ext section holds per-instruction metadata that cannot live in the flat type table: function-info, line-info, and CO-RE relocation records. Its header is (kernel.org BTF doc):
struct btf_ext_header {
__u16 magic;
__u8 version;
__u8 flags;
__u32 hdr_len;
__u32 func_info_off; __u32 func_info_len;
__u32 line_info_off; __u32 line_info_len;
__u32 core_relo_off; __u32 core_relo_len;
};Func-info associates a range of instructions with the BTF_KIND_FUNC that defines them, via struct bpf_func_info { __u32 insn_off; __u32 type_id; } — insn_off is the instruction where a function begins and type_id points at its FUNC record. Line-info maps instructions back to source via struct bpf_line_info { __u32 insn_off; __u32 file_name_off; __u32 line_off; __u32 line_col; }, where line_col packs the line number (high 22 bits) and column (low 10 bits): BPF_LINE_INFO_LINE_NUM(lc) = lc >> 10 and BPF_LINE_INFO_LINE_COL(lc) = lc & 0x3ff. The constraint that “the first insn in each func must have a line_info record pointing to it” guarantees the verifier can always name a source location.
One subtlety the documentation calls out explicitly: insn_off is interpreted differently by the kernel and by ELF loaders. “For kernel API, the insn_off is the instruction offset in the unit of struct bpf_insn. For ELF API, the insn_off is the byte offset from the beginning of section” (v6.12 BTF doc). libbpf converts the ELF byte-offset form into the kernel’s instruction-index form when it loads. The core_relo subsection carries CO-RE relocation records, covered below.
All three sub-sections share one repeating shape, which is worth drawing because it explains why .BTF.ext is versionable in a way .BTF is not:
flowchart TD H["btf_ext_header (24 bytes)<br/>magic, version, flags, hdr_len<br/>func_info_off / _len<br/>line_info_off / _len<br/>core_relo_off / _len"] H --> FI["func_info sub-section"] H --> LI["line_info sub-section"] H --> CR["core_relo sub-section"] FI --> FIR["u32 func_info_rec_size"] FIR --> FIS["btf_ext_info_sec for '.text'<br/>btf_ext_info_sec for 'fentry/tcp_connect'<br/>..."] FIS --> SEC["struct btf_ext_info_sec {<br/>u32 sec_name_off;<br/>u32 num_info;<br/>u8 data[]; }<br/>followed by num_info x rec_size bytes"] LI --> LIR["u32 line_info_rec_size"] --> LIS["one btf_ext_info_sec per ELF section"] CR --> CRR["u32 core_relo_rec_size"] --> CRS["one btf_ext_info_sec per ELF section"]
The .BTF.ext section’s uniform three-level shape. What it shows: header → three sub-sections → each begins with a record size word → each then holds one btf_ext_info_sec per ELF section, and each of those is a section-name offset, a count, and that many fixed-size records. The insight to take: the leading rec_size word is a forward-compatibility hinge, and it is the reason .BTF.ext can grow while old loaders keep working. A loader reads rec_size, and if it is larger than the struct the loader knows about, it reads the fields it understands and skips the remainder by stride — the same trick BPF_PROG_LOAD uses with func_info_rec_size and line_info_rec_size in union bpf_attr. The .BTF section has no such escape hatch, which is exactly why new type information arrives as new kinds (FLOAT, DECL_TAG, TYPE_TAG, ENUM64) rather than as new fields on existing ones.
Note the records are grouped per ELF section, not globally: btf_ext_info_sec->sec_name_off names the section (.text, or a SEC()-annotated program section like fentry/tcp_connect) that the following records apply to. This grouping is what lets libbpf load a single object file containing several independent programs and hand the kernel only the func-info and line-info belonging to the one program it is loading.
CO-RE relocation records — what the compiler leaves behind
The core_relo sub-section is where BTF stops describing types and starts describing edits. When Clang compiles BPF_CORE_READ(sk, __sk_common.skc_dport) — or any access through a __attribute__((preserve_access_index)) type — it does not bake the field’s byte offset into the load instruction. It emits a placeholder offset plus a relocation record saying “this instruction’s offset field is really the byte offset of this field path in this type, and someone should compute it against the target kernel.” The record is four words (v6.12 llvm_reloc.rst):
struct bpf_core_relo {
__u32 insn_off; /* byte offset of the instruction within its code section */
__u32 type_id; /* BTF id of the "root" containing type, in the LOCAL blob */
__u32 access_str_off; /* offset into the LOCAL .BTF string section */
enum bpf_core_relo_kind kind;
};access_str_off is the clever part. It points at a string in the object’s own string section that encodes the access path as colon-separated indices, “conceptually very close to LLVM’s getelementptr instruction’s arguments.” The documentation’s worked example is worth reproducing because the encoding is not guessable: for struct sample { int a; int b; struct { int c[10]; }; }, access to s->a is "0:0" (element 0 of s, then field index 0), s->b is "0:1", and s[1].c[5] is "1:2:0:5" — element 1 of s, field 2 (the anonymous struct), field 0 within it (c), array element 5. Field indices, not names, and not offsets. For type-based relocations the string is just "0"; for enum-value relocations it is the index of the enumerator within its enum.
Which instruction field gets patched depends on the instruction class, and the rule is mechanical: “For BPF_ALU, BPF_ALU64, BPF_LD immediate field is patched; for BPF_LDX, BPF_STX, BPF_ST offset field is patched; BPF_JMP, BPF_JMP32 instructions should not be patched” (same source). There are thirteen relocation kinds in three families:
| Family | Kinds | Answers the question |
|---|---|---|
| Field-based | FIELD_BYTE_OFFSET (0), FIELD_BYTE_SIZE (1), FIELD_EXISTS (2), FIELD_SIGNED (3), FIELD_LSHIFT_U64 (4), FIELD_RSHIFT_U64 (5) | where is this field, how big is it, does it even exist here, is it signed, and how do I shift a bitfield out |
| Type-based | TYPE_ID_LOCAL (6), TYPE_ID_TARGET (7), TYPE_EXISTS (8), TYPE_SIZE (9), TYPE_MATCHES (12) | does this type exist on the target, what is its id there, how big is it |
| Enum-based | ENUMVAL_EXISTS (10), ENUMVAL_VALUE (11) | does this enumerator exist, and what is its numeric value here |
The two bitfield-shift kinds exist because a bitfield cannot be read with a single sized load. The documentation gives the exact algorithm the compiler generates around them: relocate signed, byte_offset, byte_size, lshift and rshift, load byte_size bytes from byte_offset, then v <<= l; v >>= r. Five relocations to read one bitfield — and the reason is that the bit position of a bitfield is precisely the thing most likely to move between kernel configurations.
Resolution happens entirely in userspace, at load time, before the program ever reaches the verifier:
flowchart TD START["libbpf reads one bpf_core_relo"] --> LOCAL["look up type_id in the OBJECT's .BTF<br/>-> local type, e.g. 'struct sock'"] LOCAL --> NAME["read its name from the object's string section"] NAME --> CAND["search TARGET BTF (vmlinux + modules)<br/>for candidate types with a compatible name"] CAND --> ANY{"any candidate?"} ANY -->|no| KINDQ{"is the kind an<br/>EXISTS / MATCHES probe?"} KINDQ -->|yes| ZERO["patch immediate to 0<br/>program compiles the field out"] KINDQ -->|no| FAIL["relocation fails -> load error"] ANY -->|yes| WALK["walk the access string '0:2:0:5'<br/>index by index through the TARGET type"] WALK --> AGREE{"do all candidates<br/>agree on the answer?"} AGREE -->|no| AMBIG["ambiguous relocation -> load error"] AGREE -->|yes| PATCH["write the computed value into the<br/>instruction's imm or off field"] PATCH --> NEXT["next relocation"] ZERO --> NEXT
CO-RE relocation resolution, per record, at load time. What it shows: each relocation is resolved by name-matching the local root type into the target kernel’s BTF, then re-walking the same index path through the target’s member list to compute a fresh answer. The insight to take: two failure modes fall straight out of this diagram, and they explain most real CO-RE errors. First, EXISTS-family relocations cannot fail — a missing field patches to 0, which is what makes bpf_core_field_exists() work as a runtime feature test and lets one binary carry both an old and a new code path. Every other kind is fatal if unresolvable. Second, resolution is a search, not a lookup: several candidate types may share a name (split BTF across modules makes this common), and if they disagree about the offset, libbpf cannot pick one and refuses. The field-access details and the BPF_CORE_READ macro machinery live in CO-RE Relocations and Field Access; what matters here is that all of it is driven by four-word records sitting in the object’s .BTF.ext, resolved against the type table this note describes.
The .BTF_ids section — BTF inside the kernel’s own build
There is a third BTF-bearing section, and it points the other way. .BTF_ids does not describe the kernel’s types to userspace; it lets kernel C code refer to a BTF type by id. Kernel source writes a list or a sorted set with macros from include/linux/btf_ids.h:
BTF_ID_LIST(bpf_skb_output_btf_ids)
BTF_ID(struct, sk_buff)
BTF_ID_UNUSED /* four zero bytes, a reserved slot */
BTF_ID(struct, task_struct)
BTF_SET_START(btf_id_deny) /* a sorted set, searchable with btf_id_set_contains() */
BTF_ID(func, migrate_disable)
BTF_ID(func, migrate_enable)
BTF_SET_END(btf_id_deny)Each macro emits four zero bytes at a specially-named symbol (__BTF_ID__struct__sk_buff__1), and “all the BTF ID lists and sets are compiled in the .BTF_ids section and resolved during the linking phase of kernel build by resolve_btfids” (v6.12 btf.rst §4.2). The typeX argument — one of struct, union, typedef, func — acts as a filter when the tool resolves the name. So the zeroes become real BTF ids only after the vmlinux BTF blob exists, which is why this is a link-time step and not a compile-time one.
This matters far beyond bookkeeping: it is how the kernel expresses policy about types in ordinary C. The btf_id_deny set above is a real example from kernel/bpf/verifier.c — the list of kernel functions no tracing program may attach to — and it is checked with a single btf_id_set_contains(&btf_id_deny, btf_id) call at attach time. See fentry fexit and BPF Trampolines for what is on that list and why.
How the Kernel and Verifier Use BTF
Before enumerating the jobs, it helps to see the whole pipeline once, because BTF is produced twice, by two different tools, and consumed at two different moments:
sequenceDiagram autonumber participant K as Kernel build participant C as Clang participant L as libbpf participant S as bpf syscall participant V as Verifier K->>K: build vmlinux with DWARF K->>K: pahole -J converts DWARF to deduplicated BTF K->>K: objcopy puts .BTF in the image as SHF_ALLOC K->>K: linker defines __start_BTF and __stop_BTF Note over K: at boot, sysfs exposes that range<br/>read-only as /sys/kernel/btf/vmlinux C->>C: compile prog.bpf.c with -g --target=bpf C-->>L: object with .BTF (its own types)<br/>and .BTF.ext (func, line, core_relo) L->>K: open and parse the target kernel's BTF L->>L: resolve every core_relo against target BTF L->>L: patch imm and off fields in the instructions L->>S: BPF_BTF_LOAD with the object's own .BTF blob S-->>L: btf_fd L->>S: BPF_MAP_CREATE with btf_fd, key and value type ids S->>S: map_check_btf validates sizes, builds btf_record L->>S: BPF_PROG_LOAD with prog_btf_fd, func_info, line_info S->>V: verify V->>V: check_btf_info validates func and line info V->>V: check_attach_btf_id resolves the attach target V-->>L: accepted, or a log annotated with real source lines
The full BTF lifecycle, from two producers to two consumers. What it shows: the kernel’s BTF is made once, at kernel build time, by pahole reading DWARF; the program’s BTF is made at program compile time, by Clang. They meet in libbpf, which reads the first to fix up code described by the second, then hands the second to the kernel. The insight to take: the relocation step (11-12) happens entirely in userspace, before the kernel sees a single instruction. The kernel never resolves a CO-RE relocation and has no idea one existed — by the time BPF_PROG_LOAD is called, the offsets are ordinary constants. This is why CO-RE needs no kernel support beyond exposing the BTF blob, and why it worked on kernels far older than CO-RE itself as long as they had CONFIG_DEBUG_INFO_BTF=y. The kernel’s own BTF consumption (steps 16-20) is a separate concern entirely: type-checking maps, and resolving attach targets by signature.
BTF is not decorative metadata that the kernel ignores; the verifier and the bpf() syscall actively consume it for four jobs.
First, typing map keys and values. When userspace creates a map it can pass btf_fd, btf_key_type_id, and btf_value_type_id in the BPF_MAP_CREATE attributes; libbpf “is able to extract key/value type_id’s and assign them to BPF_MAP_CREATE attributes automatically” from the BTF-defined map declaration in the object (v6.12 BTF doc). With the value type known, bpftool map dump and the kernel’s own map-printing can render a value as a named struct with fields instead of an opaque byte array — the original motivating use case for BTF, and still the most visible one. The kernel documentation shows the payoff on a struct that is genuinely unreadable as bytes, a struct tmp_t of nested bitfields, printing as {"a1": 0x2, "a2": 0x4, "a3": 0x6, "b": 7, "b1": 0x8, "b2": 0xa} (v6.12 btf.rst §5.1).
But typed values buy something far more important than pretty-printing, and this is the part that surprises people: BTF is how the kernel finds special objects hidden inside an otherwise opaque map value. A map value is, to the map layer, value_size anonymous bytes. Yet a value may legally embed a bpf_spin_lock, a bpf_timer, a bpf_wq workqueue, a kernel pointer (kptr), a refcount, or the head of a BPF linked list or red-black tree — objects the kernel must initialize, copy specially, and destroy. It locates them by walking the value’s BTF.
The walk happens in map_check_btf() in kernel/bpf/syscall.c at BPF_MAP_CREATE time, and it is strict about sizes first (v6.12 syscall.c):
if (btf_key_id) {
key_type = btf_type_id_size(btf, &btf_key_id, &key_size);
if (!key_type || key_size != map->key_size) /* BTF must agree with key_size */
return -EINVAL;
}
value_type = btf_type_id_size(btf, &btf_value_id, &value_size);
if (!value_type || value_size != map->value_size) /* ...and with value_size */
return -EINVAL;
map->record = btf_parse_fields(btf, value_type,
BPF_SPIN_LOCK | BPF_TIMER | BPF_KPTR | BPF_LIST_HEAD |
BPF_RB_ROOT | BPF_REFCOUNT | BPF_WORKQUEUE,
map->value_size);Line by line: the BTF type’s computed size must exactly equal the key_size/value_size the caller passed in union bpf_attr, or the create fails with -EINVAL — BTF is not allowed to describe a different object than the one being allocated. Keys may be left untyped (btf_key_id == 0) for map types whose map_check_btf op permits it; values may not. Then btf_parse_fields() scans the value type for the seven interesting field classes and returns a struct btf_record.
How does it recognize a spin lock? By string-comparing the member’s type name, in btf_get_field_type() (v6.12 kernel/bpf/btf.c):
const char *name = __btf_name_by_offset(btf, var_type->name_off);
if (field_mask & BPF_SPIN_LOCK) {
if (!strcmp(name, "bpf_spin_lock")) {
if (*seen_mask & BPF_SPIN_LOCK)
return -E2BIG; /* at most one per value */
*seen_mask |= BPF_SPIN_LOCK;
type = BPF_SPIN_LOCK;
goto end;
}
}
/* ... bpf_timer, bpf_wq, then by macro: */
field_mask_test_name(BPF_LIST_HEAD, "bpf_list_head");
field_mask_test_name(BPF_RB_ROOT, "bpf_rb_root");
field_mask_test_name(BPF_REFCOUNT, "bpf_refcount");
/* Only return BPF_KPTR when all other types with matchable names fail */
if (field_mask & BPF_KPTR && !__btf_type_is_struct(var_type))
type = BPF_KPTR_REF;That is the entire mechanism: a strcmp against a literal type name, resolved through the BTF string section. There is no registry, no annotation, no magic attribute — you get a working spin lock in your map value by naming the type bpf_spin_lock, and BTF is the only thing that carries that name from your source file into the kernel. The seen_mask check enforces at most one lock, one timer and one workqueue per value; a value may hold at most BTF_FIELDS_MAX (11) special fields in total.
flowchart TD SRC["struct my_val {<br/> __u64 count;<br/> struct bpf_spin_lock lock;<br/> struct task_struct __kptr *owner;<br/>};"] --> CLANG["clang emits BTF:<br/>STRUCT my_val, vlen 3,<br/>member offsets in bits"] CLANG --> LIBBPF["libbpf reads the .maps DATASEC,<br/>extracts btf_key_type_id and<br/>btf_value_type_id from __type()"] LIBBPF --> CREATE["BPF_MAP_CREATE<br/>btf_fd, btf_key_type_id, btf_value_type_id"] CREATE --> CHECK["map_check_btf()"] CHECK --> SZ{"BTF size ==<br/>key_size / value_size?"} SZ -->|no| EINVAL["-EINVAL"] SZ -->|yes| PARSE["btf_parse_fields()<br/>walk members, strcmp type names"] PARSE --> REC["struct btf_record<br/>lock at byte 8<br/>kptr at byte 16 (btf_id of task_struct)"] REC --> CAPS{"caller has CAP_BPF?<br/>map not RDONLY_PROG/WRONLY_PROG?"} CAPS -->|no| DENY["-EPERM / -EACCES"] CAPS -->|yes| TYPEOK{"does THIS map type<br/>allow that field?"} TYPEOK -->|no| NOTSUP["-EOPNOTSUPP"] TYPEOK -->|yes| LIVE["map live: copy_map_value()<br/>now knows to skip the lock<br/>and refcount the kptr"]
From a C struct to a btf_record, and the four ways it can be refused. What it shows: the special-field offsets the map layer needs at runtime are derived, at map-create time, by walking BTF and name-matching member types — then gated by capability, by map flags, and by whether this particular map type supports that field class. The insight to take: the last gate is the one that bites. map_check_btf() carries an explicit per-field allow-list of map types: a bpf_spin_lock is permitted only in HASH, ARRAY and the storage maps; a bpf_timer or bpf_wq only in HASH, LRU_HASH and ARRAY; bpf_list_head/bpf_rb_root likewise. Put a timer in a PERCPU_ARRAY and the create fails -EOPNOTSUPP even though the BTF was perfectly valid — and the default arm of that switch returns -EOPNOTSUPP too, deliberately, so that adding a new field type without adding its map-type check fails closed rather than silently allowing everything. This is also why copy_map_value() exists instead of a plain memcpy: with a btf_record in hand the map layer knows which byte ranges inside a value are objects rather than data.
Second, typing global variables. BPF global variables compile into the object’s .data, .bss, and .rodata sections. BTF represents each section as a BTF_KIND_DATASEC whose name_off points at the section name, containing one btf_var_secinfo per variable, each of which references a BTF_KIND_VAR for the variable and the variable’s actual type (v6.12 BTF doc). The kernel turns these sections into internal “global data” maps; without the DATASEC/VAR records the verifier could not know the type and bounds of my_global_counter, so this is what makes BPF global variables type-safe.
Third, func-info and line-info for the verifier log. Because the program ships line-info, the verifier can annotate its output with the exact C source line of an offending instruction. This is why a modern verifier rejection points at prog.bpf.c:42 rather than a bare instruction index — line-info “helps generate source annotated translated byte code, jited code and verifier log” (kernel.org BTF doc). See Reading and Debugging Verifier Errors for how to read those annotated logs.
Fourth, resolving kfuncs, struct_ops, and fentry/fexit targets by name. These features call or attach to specific kernel functions identified by name and signature rather than a numbered stable ABI; the kernel matches the program’s BTF function prototype against its own vmlinux BTF to verify the signatures line up. This is the BTF dependency behind kfuncs and struct_ops/sched_ext.
BTF_KIND_FUNC_PROTO is what makes fentry type-safe
The fourth job deserves unpacking, because it is where BTF stops being metadata and starts being load-bearing for code generation. When you write SEC("fentry/tcp_connect") int BPF_PROG(f, struct sock *sk), the kernel must answer two questions before a single instruction runs: does tcp_connect exist as a real, traceable function? and what does its signature look like, in registers? Both answers come from BTF, and the path between them is short and mechanical.
flowchart TD ID["attach_btf_id (u32) from BPF_PROG_LOAD"] --> T["btf_type_by_id(vmlinux_btf, id)"] T --> ISFUNC{"btf_type_is_func(t)?"} ISFUNC -->|no| E1["'attach_btf_id %u is not a function'<br/>-EINVAL"] ISFUNC -->|yes| NAME["tname = name_off -> 'tcp_connect'"] ISFUNC -->|yes| PROTO["t = btf_type_by_id(btf, t->type)<br/>the FUNC_PROTO"] PROTO --> ISPROTO{"btf_type_is_func_proto(t)?"} ISPROTO -->|no| E2["-EINVAL"] ISPROTO -->|yes| DISTILL["btf_distill_func_proto()"] DISTILL --> MODEL["struct btf_func_model {<br/>u8 nr_args;<br/>u8 arg_size[12];<br/>u8 arg_flags[12];<br/>u8 ret_size; }"] NAME --> KSYM["kallsyms_lookup_name(tname)<br/>or find_kallsyms_symbol_value(mod, tname)"] KSYM --> ADDR["runtime address, or<br/>'The address of function %s cannot be found' -ENOENT"] MODEL --> TRAMP["trampoline generator emits exactly<br/>nr_args register spills, no more"] ADDR --> TRAMP
From a 32-bit BTF id to a machine-code stub. What it shows: check_attach_btf_id() uses BTF for the shape of the target and kallsyms for its address — two independent lookups keyed by the same name. The BTF half produces a btf_func_model, a flattened summary of the signature: how many arguments and how many bytes each occupies. The insight to take: this is why fentry argument access needs no bpf_probe_read() and no pt_regs arithmetic. The trampoline generator receives nr_args and arg_size[] and emits precisely that many mov [rbp-N], reg spills, laying the arguments out as a flat u64 args[] array that the BPF program indexes directly. A kprobe cannot do this because a kprobe has only a struct pt_regs and no idea which registers are live arguments — the type information simply is not there. Note the fallback arm in btf_distill_func_proto(): when it is handed a null prototype it assumes MAX_BPF_FUNC_REG_ARGS (5) arguments of 8 bytes each and a 8-byte return, i.e. it degrades to the untyped, register-shaped view.
btf_distill_func_proto() also rejects signatures the trampoline cannot express, and its error strings are worth memorizing because they are what you will actually see (v6.12 kernel/bpf/btf.c):
| Condition | Message |
|---|---|
vlen > MAX_BPF_FUNC_ARGS (12) | The function %s has %d arguments. Too many. |
| return type is a struct (by value) | The function %s return type %s is unsupported. |
last param has type == 0 (variadic) | The function %s with variable args is unsupported. |
| a struct argument larger than 16 bytes | The function %s arg%d type %s is unsupported. |
| an argument of size 0 | The function %s has malformed void argument. |
Every one of these is a BTF constraint expressed at attach time — the function exists and is perfectly callable from C, but its FUNC_PROTO describes something the generated stub cannot marshal. See fentry fexit and BPF Trampolines for what the generator does with the model once it has one.
Where Kernel BTF Comes From: pahole and CONFIG_DEBUG_INFO_BTF
Kernel BTF is generated at kernel build time by pahole (from the dwarves package), which reads the kernel’s DWARF debug info, converts it to BTF, and deduplicates it. This is gated by CONFIG_DEBUG_INFO_BTF. The 6.12 Kconfig entry is precise about the dependencies (v6.12 lib/Kconfig.debug):
config DEBUG_INFO_BTF
bool "Generate BTF type information"
depends on !DEBUG_INFO_SPLIT && !DEBUG_INFO_REDUCED
depends on !GCC_PLUGIN_RANDSTRUCT || COMPILE_TEST
depends on BPF_SYSCALL
depends on PAHOLE_VERSION >= 116
depends on DEBUG_INFO_DWARF4 || PAHOLE_VERSION >= 121
The help text states it will “convert DWARF type info into equivalent deduplicated BTF type info,” requiring pahole v1.16 or later (v1.21 or later to support DWARF 5). The dependency on !DEBUG_INFO_REDUCED and !DEBUG_INFO_SPLIT exists because pahole needs the full DWARF to produce complete BTF. Once built, the kernel exposes the result at /sys/kernel/btf/vmlinux as a raw BTF blob any tool can read.
The plumbing between “pahole ran” and “sysfs has a file” is three steps in the link script and nine lines of C, and reading them removes any remaining mystery about where the blob lives. In scripts/link-vmlinux.sh, gen_btf() runs pahole -J over the linked vmlinux, then extracts the result (v6.12 link-vmlinux.sh):
${OBJCOPY} --only-section=.BTF --set-section-flags .BTF=alloc,readonly \
--strip-all ${1} "${btf_data}" 2>/dev/nullThe critical flag is alloc. Setting SHF_ALLOC on .BTF is what promotes it from a debug section that the loader discards into part of the loaded kernel image — the comment says so directly: “Add SHF_ALLOC because .BTF will be part of the vmlinux image.” The --strip-all then deletes every symbol including __start_BTF and __stop_BTF, “which will be redefined in the linker script” — the linker script re-emits them as the bounds of the section in the final image. And that is exactly what sysfs reads (v6.12 kernel/bpf/sysfs_btf.c):
extern char __start_BTF[];
extern char __stop_BTF[];
static ssize_t btf_vmlinux_read(struct file *file, struct kobject *kobj,
struct bin_attribute *bin_attr,
char *buf, loff_t off, size_t len)
{
memcpy(buf, __start_BTF + off, len); /* the blob IS the section */
return len;
}
static int __init btf_vmlinux_init(void)
{
bin_attr_btf_vmlinux.size = __stop_BTF - __start_BTF;
if (bin_attr_btf_vmlinux.size == 0)
return 0; /* no BTF -> no sysfs entry at all */
btf_kobj = kobject_create_and_add("btf", kernel_kobj);
...
return sysfs_create_bin_file(btf_kobj, &bin_attr_btf_vmlinux);
}So /sys/kernel/btf/vmlinux is not a generated view or a serialization — it is a raw memcpy out of a read-only section of the running kernel image, exposed as a mode-0444 binary sysfs attribute. Two consequences follow. First, cat /sys/kernel/btf/vmlinux | wc -c tells you the exact number of bytes of kernel image spent on BTF. Second, when CONFIG_DEBUG_INFO_BTF=n the section is empty, size == 0, and btf_vmlinux_init() returns early without creating the btf kobject — which is why on a BTF-less kernel the whole /sys/kernel/btf/ directory is missing, not merely the vmlinux file inside it.
Per-module BTF is its own config, DEBUG_INFO_BTF_MODULES, which defaults to y and depends on DEBUG_INFO_BTF && MODULES && PAHOLE_HAS_SPLIT_BTF (v6.12 Kconfig.debug). PAHOLE_HAS_SPLIT_BTF is def_bool PAHOLE_VERSION >= 119, so split BTF needs pahole v1.19+. The output is “compact split BTF type information for kernel modules”: each loadable module gets its own BTF blob at /sys/kernel/btf/<module-name> that is built incrementally on top of the vmlinux base. A module’s BTF only contains the types the module introduces; references to core kernel types resolve into the vmlinux BTF, which keeps module BTF tiny. This is why bpftool requires a --base-btf /sys/kernel/btf/vmlinux argument to interpret a module’s BTF — the split blob is meaningless without its base (bpftool-btf docs).
The per-module naming convention is not a convention at all but literal code, and it can be read at the v6.12 tag. Module BTF is registered by a module notifier, btf_module_notify() in kernel/bpf/btf.c, which fires on MODULE_STATE_COMING (v6.12 kernel/bpf/btf.c):
btf = btf_parse_module(mod->name, mod->btf_data, mod->btf_data_size,
mod->btf_base_data, mod->btf_base_data_size);
...
if (IS_ENABLED(CONFIG_SYSFS)) {
attr = kzalloc(sizeof(*attr), GFP_KERNEL);
sysfs_bin_attr_init(attr);
attr->attr.name = btf->name; /* == the module name */
attr->attr.mode = 0444;
attr->size = btf->data_size;
attr->private = btf;
attr->read = btf_module_read;
err = sysfs_create_bin_file(btf_kobj, attr); /* under /sys/kernel/btf/ */
}btf_kobj is the same kobject sysfs_btf.c created for vmlinux, and attr->attr.name is the module’s own name, so a loaded bridge module produces /sys/kernel/btf/bridge with mode 0444 — the file appears when the module loads and disappears (sysfs_remove_bin_file) when it goes. Note the two extra arguments to btf_parse_module(): mod->btf_base_data and its size. That is the split-BTF base being handed in explicitly, which is what makes the module blob’s type IDs resolvable.
There is also a failure mode encoded here worth knowing. If the module’s BTF cannot be validated against the base — the classic cause being a module built against different kernel headers than the running kernel — btf_parse_module() returns an error, and what happens next depends on CONFIG_MODULE_ALLOW_BTF_MISMATCH: with it disabled the module fails to load with failed to validate module [%s] BTF; with it enabled the kernel logs Kernel module BTF mismatch detected, BTF debug info may be unavailable for some modules once and carries on without that module’s BTF. Out-of-tree and DKMS-built modules are where this shows up.
flowchart LR subgraph BASE["/sys/kernel/btf/vmlinux — base BTF"] B1["ID 1: INT 'int'"] B2["ID 2: PTR -> 1"] BD["... ~100k types ..."] BN["ID N: STRUCT 'sk_buff'"] end subgraph MOD["/sys/kernel/btf/bridge — split BTF"] M1["ID N+1: STRUCT 'net_bridge'"] M2["ID N+2: PTR -> N+1"] M3["ID N+3: STRUCT 'net_bridge_port'<br/>member 'dev' -> type ID N"] end M3 -.->|"resolves into the base"| BN M2 -.-> M1 NOTE["bpftool btf dump file /sys/kernel/btf/bridge<br/>--base-btf /sys/kernel/btf/vmlinux"]:::n classDef n fill:#f6f6f6,stroke:#bbb
Split BTF: a module’s blob is an ID-space continuation of the base, not a standalone table. What it shows: the base blob numbers types 1..N; the module blob’s first type is N+1, and any reference to a core kernel type is simply an ID below N+1 that only the base can resolve. The insight to take: this is why a module’s BTF is a few kilobytes rather than a few megabytes — it stores only the types the module introduces. It is also why the base is not optional: hand a consumer a module blob alone and every ID below N+1 dangles. bpftool takes an explicit base with -B / --base-btf, though as of its current documentation (read 2026-09-04, libbpf/bpftool main) it has a convenience path too — “when sysfs paths are used, vmlinux BTF is loaded automatically as the base; if vmlinux itself appears in the file list it is skipped” (bpftool-btf docs). Point a tool at a raw copy of a module blob sitting somewhere other than /sys/kernel/btf/ and you are back to supplying the base by hand. The same constraint applies inside the kernel: the base pointer is passed into btf_parse_module() at load time and the module’s struct btf holds a reference to it for as long as the module is live.
BTF Deduplication — Why vmlinux BTF Is Small
The reason kernel BTF is a few megabytes and not a hundred is deduplication, performed by pahole at build time and implemented in libbpf’s btf__dedup for tooling. DWARF emits type information independently for every compilation unit: each .o that references struct task_struct re-describes it and everything it transitively references “down to primitive types like int and long” (Nakryiko, BTF Dedup). Concatenating thousands of compilation units’ BTF therefore contains thousands of copies of struct task_struct. Deduplication collapses all structurally-identical types to one canonical record and rewrites every reference to point at it, and additionally merges forward declarations against their full definitions across compilation units. The measured effect on a 4.11 kernel — from 101.7 MB of raw type descriptors down to 0.97 MB after dedup, a 104x reduction — is what makes shipping kernel types inside the kernel feasible at all (Nakryiko, BTF Dedup).
flowchart TD subgraph BEFORE["Before dedup: DWARF, per compilation unit"] CU1["net/core/dev.o<br/>STRUCT sk_buff<br/>STRUCT net_device<br/>INT int, INT long ..."] CU2["fs/read_write.o<br/>STRUCT sk_buff<br/>STRUCT file<br/>INT int, INT long ..."] CU3["... x thousands of .o files ..."] end BEFORE --> N1["3,150,000 type descriptors<br/>101.7 MB"] N1 --> DEDUP["btf__dedup:<br/>1. hash each type structurally<br/>2. collapse identical types to one canonical record<br/>3. rewrite every referencing ID<br/>4. merge FWD declarations onto full definitions"] DEDUP --> N2["22,898 type descriptors<br/>0.97 MB"] N2 --> AFTER["One STRUCT sk_buff.<br/>One INT int.<br/>Every reference points at it."]
Why deduplication is the whole ballgame. What it shows: DWARF re-describes every type in every compilation unit that touches it, transitively down to int; concatenating a kernel’s worth of that produces millions of near-identical records. Structural hashing collapses them to one canonical copy each. The two measurements are from a Linux 4.11 build. The insight to take: the compression ratio is not 104x because the data compresses well — it is 104x because 99% of it was redundant, and the redundancy is inherent to how DWARF is emitted, not a defect. This is also why the BTF format has no compression of its own and needs none. The step that merges forward declarations onto full definitions is the subtle one: a compilation unit that only ever sees struct sock; emits a FWD, and without that merge the deduplicated table would carry both an empty FWD sock and a full STRUCT sock, leaving consumers to guess which one a given ID meant.
Failure Modes and Common Misunderstandings
The most common operational failure is a missing /sys/kernel/btf/vmlinux: the kernel was built without CONFIG_DEBUG_INFO_BTF, or the build host’s pahole was too old (older than v1.16) and BTF generation was silently skipped. Any CO-RE-based tool then fails to load with a “failed to find valid kernel BTF” or “BTF is required, but is missing or corrupted” error. The standard remedy is an external BTF archive such as BTFHub, which provides pre-generated BTF blobs for kernels that shipped without their own, fed to libbpf via the “BTF override” path (docs.ebpf.io BTF).
A subtler trap is treating module BTF as standalone. Because module BTF is split on the vmlinux base, dumping or loading it without the base BTF yields garbage type IDs — every reference into the core kernel is unresolved. Tools must always supply the base.
Finally, BTF records types, not macros. The documentation and Nakryiko both note BTF “doesn’t record #define macros” (Nakryiko, BPF CO-RE). A generated vmlinux.h therefore has every struct and enum but none of the #define FOO 0x1 constants from kernel headers; programs must define those themselves or pull them from libbpf’s helper headers. This is the practical seam handled in BTF and vmlinux.h.
Putting the operational failures on one decision path turns a class of confusing load errors into a two-minute diagnosis:
flowchart TD START["a CO-RE tool fails to load"] --> DIR{"does /sys/kernel/btf/<br/>exist at all?"} DIR -->|no| NOBTF["kernel built without<br/>CONFIG_DEBUG_INFO_BTF<br/>(or pahole was too old at build time,<br/>so the .BTF section is empty and<br/>btf_vmlinux_init() returned early)"] NOBTF --> HUB["fix: external BTF archive (BTFHub)<br/>fed to libbpf via the BTF-override path"] DIR -->|yes| VM{"is /sys/kernel/btf/vmlinux<br/>non-empty?"} VM -->|no| NOBTF VM -->|yes| MOD{"is the target type or function<br/>defined in a MODULE?"} MOD -->|yes| MODBTF{"does /sys/kernel/btf/<mod><br/>exist?"} MODBTF -->|no| MISMATCH["CONFIG_DEBUG_INFO_BTF_MODULES=n,<br/>or BTF mismatch tolerated via<br/>CONFIG_MODULE_ALLOW_BTF_MISMATCH"] MODBTF -->|yes| BASE["ensure the consumer was given<br/>the vmlinux base BTF"] MOD -->|no| MACRO{"is the missing thing a<br/>#define, not a type?"} MACRO -->|yes| NOMAC["BTF does not record macros.<br/>Define it yourself or pull it<br/>from libbpf's helper headers."] MACRO -->|no| RELO["a CO-RE relocation genuinely<br/>failed: the field or type does not<br/>exist on this kernel.<br/>Guard it with bpf_core_field_exists()."]
A diagnosis path for “BTF is missing or wrong”. What it shows: four genuinely different failures that all surface as a load error — no kernel BTF at all, no module BTF, a #define that BTF never carried, and a relocation that legitimately cannot resolve. The insight to take: the very first check is the directory, not the file. Because btf_vmlinux_init() returns before creating the kobject when the .BTF section is empty, a CONFIG_DEBUG_INFO_BTF=n kernel has no /sys/kernel/btf/ directory — so ls -l /sys/kernel/btf/vmlinux reporting “No such file or directory” is ambiguous between “no BTF” and “typo”, while ls -d /sys/kernel/btf/ is not. The last branch is the one people misdiagnose as a bug: a relocation failing because a field really was renamed between kernel versions is CO-RE working correctly and telling you so, and the fix is a feature test in the program, not a change to the toolchain.
Uncertain
Verify: whether
Documentation/bpf/btf.rst§6 is stale at the v6.12 tag. It states that pahole “acts as a dwarf2btf converter. It doesn’t support .BTF.ext and btf BTF_KIND_FUNC type yet.” This is contradicted by kernel code at the same tag:check_attach_btf_id()inkernel/bpf/verifier.crequires theattach_btf_idof anfentry/fexittarget to satisfybtf_type_is_func()against vmlinux BTF, and vmlinux BTF is produced bypahole -J— so pahole must emitBTF_KIND_FUNC, or nofentryattach could ever succeed.DEBUG_INFO_BTFalso requires pahole ≥ v1.16, far newer than the era that sentence describes. Reason: an in-tree documentation sentence that its own tree’s code contradicts; the sentence appears to be un-updated text from the format’s introduction. To resolve: check thepaholerelease notes for whenBTF_KIND_FUNCemission landed, and check whether the sentence has been removed in a later kernel. Treat §6’s examples as sound (they were decoded field-by-field above and are self-consistent) and its capability claim about pahole as stale. uncertain
Inspecting BTF in Practice: bpftool btf
Nothing above is reachable without a tool that reads the blob, and that tool is bpftool btf. It takes a BTF source — a loaded BTF object by id, a program, a map, or a raw file — and dumps it in one of two formats (bpftool-btf docs, read 2026-09-04):
# What BTF objects does this kernel currently hold?
$ bpftool btf show
# (since Linux 5.8 this also reports which processes hold FDs against each object)
# Dump the running kernel's own types, in the raw record-by-record form
$ bpftool btf dump file /sys/kernel/btf/vmlinux format raw
# The same, rendered as compilable C -- this is how vmlinux.h is generated
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# One type and only its dependencies, which is what you actually want
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c root_id 1234
# A module's split BTF (vmlinux is picked up as the base automatically
# for sysfs paths; -B names a base explicitly for files elsewhere)
$ bpftool btf dump file /sys/kernel/btf/bridge format c
# Just the key and value types of a live map -- 'kv' is the default
$ bpftool btf dump map id 42 kvReading these in order: format raw prints one line per type record and is the form to reach for when you are debugging the format — it shows kinds, vlens, member offsets and type IDs, which is exactly the structure this note has been describing. format c reconstructs C declarations and is sorted by default (pass unsorted to preserve record order, which is occasionally useful for spotting how the dedup pass ordered things). root_id filters to a single type and all its dependent types, which is the difference between a 100,000-line vmlinux.h and a readable page — and it may be passed more than once. Finally, dump map is BTF closing the loop on its original motivating use case: the key and value types recorded at BPF_MAP_CREATE are read back out and used to pretty-print the map.
Alternatives and When to Choose Them
BTF’s design alternatives are DWARF and CTF. DWARF is the full-fidelity standard — choose it when you need complete debug information for gdb, source-level debugging, or variable-location tracking across the whole program; it is simply too large for the kernel to ship at runtime. CTF (Compact C Type Format), Sun’s format that inspired BTF, solved the same compactness problem for DTrace; BTF diverged to fit BPF’s specific needs (the .BTF.ext info, kind_flag bitfields, CO-RE relocations, kfunc signatures). Within the BPF world there is effectively no alternative to BTF: CO-RE, typed maps, struct_ops, kfuncs, and readable verifier logs all depend on it, so “use BTF” is not really a choice but a prerequisite for modern eBPF.
| DWARF | CTF | BTF | |
|---|---|---|---|
| Designed for | source-level debugging | DTrace type lookup | BPF, kernel-resident |
| Available at runtime in the kernel | no (stripped from production images) | n/a on Linux | yes, /sys/kernel/btf/vmlinux |
| Size, whole-kernel | ~100 MB+ | — | a few MB |
| Deduplicated | no, per compilation unit | yes | yes, pahole -J / btf__dedup |
| Records macros | yes (.debug_macro) | no | no |
| Variable locations / line tables | yes, full | no | line info only, via .BTF.ext |
| Carries relocations for code patching | no | no | yes, core_relo |
| Function signatures usable for attach | not at runtime | no | yes, FUNC + FUNC_PROTO |
The row that decides it is the second one. DWARF is strictly more expressive than BTF and always will be; what it cannot do is be present. Every other row follows from the decision to make the format small enough to ship inside the image.
One thing BTF explicitly does not buy you is stability. The kernel’s own design FAQ is blunt about this, and it matters because BTF’s ergonomics make it easy to forget: “Q: Attaching to arbitrary kernel functions is an ABI? A: NO. The kernel function prototypes will change, and BPF programs attaching to them will need to change.” And, separately: “Q: Marking a function with BTF_ID makes that function an ABI? A: NO. The BTF_ID macro does not cause a function to become part of the ABI any more than does the EXPORT_SYMBOL_GPL macro” (v6.12 bpf_design_QA.rst). BTF tells you what the kernel’s types are right now; it makes no promise about next release. What CO-RE buys is that the breakage is detected at load time and named, rather than silently reading the wrong bytes — which is a large improvement over the pre-BTF world, but is not the same as stability. The FAQ makes the same point about bpf_spin_lock/bpf_timer in map values (compatibility preserved) versus kptr and everything else (“it depends”).
Production Notes
In practice, almost no one hand-writes or hand-parses BTF — pahole produces kernel BTF, Clang produces object BTF (clang -g -target bpf emits .BTF/.BTF.ext), libbpf parses and loads it, and bpftool inspects it. The one BTF artifact engineers handle directly is the generated header from bpftool btf dump file /sys/kernel/btf/vmlinux format c, which is the subject of BTF and vmlinux.h. Distribution kernels (recent Fedora, Ubuntu, RHEL, Debian) ship CONFIG_DEBUG_INFO_BTF=y as standard, which is why modern CO-RE tools “just work” on them; embedded and older kernels are where BTFHub fallback matters. When debugging a tool that fails to load, the first diagnostic is ls -d /sys/kernel/btf/ — the directory’s presence tells you immediately whether the kernel can describe itself, and it is unambiguous in a way that checking for the vmlinux file is not.
Two operational facts are worth carrying around. First, BTF size is a real, visible budget line. Because .BTF is SHF_ALLOC, it is resident kernel memory for the life of the boot, not pageable debug data — wc -c < /sys/kernel/btf/vmlinux reports exactly what it costs, typically a few megabytes on a distribution kernel. That is trivially affordable on a server and genuinely contested on an embedded device, which is the real reason CONFIG_DEBUG_INFO_BTF is not universally enabled rather than any philosophical objection. It is also why the split-BTF design exists: making every module carry a full copy of the core types would have multiplied that budget by the number of loaded modules.
Second, the BTF you have is the BTF of the kernel you are running, and nothing else. A container does not have its own; /sys/kernel/btf/vmlinux inside a container is the host kernel’s, which is correct and is what makes CO-RE tooling work unmodified in containers. A cross-compiled or ahead-of-time-relocated program, by contrast, was relocated against some BTF at build time, and if that was not the target’s, the offsets are wrong in exactly the way CO-RE was designed to prevent — this is the failure mode behind AOT-compiled BPF, and it is why libbpf’s BTF-override path (BPFTRACE_BTF and friends, discussed from the tool side in bpftrace) exists as an explicit, opt-in escape hatch rather than a default.
The one place engineers touch BTF by hand remains the generated header from bpftool btf dump file /sys/kernel/btf/vmlinux format c, and the root_id filter is the difference between that being usable and not — see BTF and vmlinux.h.
See Also
- BTF and vmlinux.h — the practical workflow: generating and building against the single
vmlinux.hheader - CO-RE (Compile Once Run Everywhere) — the portability mechanism that consumes kernel BTF at load time
- CO-RE Relocations and Field Access — what lives in the
.BTF.extcore_relosection and how field offsets are relocated - libbpf and the BPF Loader — parses
.BTF/.BTF.ext, converts info offsets, loads viaBPF_BTF_LOAD - BPF Skeletons and bpftool —
bpftool btf dumpand the skeleton generator that read BTF - eBPF Verifier — consumer of func-info/line-info for annotated error logs and of DATASEC/VAR for typed globals
- BPF Kernel Functions (kfuncs) — resolved by matching BTF function signatures against vmlinux BTF
- struct_ops and sched_ext — uses
BTF_KIND_DECL_TAGand BTF signatures to wire up operation tables - fentry fexit and BPF Trampolines — the consumer of
BTF_KIND_FUNC_PROTO: how abtf_func_modelbecomes a generated machine-code stub - BPF Maps — the map layer that receives
btf_key_type_id,btf_value_type_idand thebtf_recordbuilt here - bpftrace — the same BTF/CO-RE story told from the tool side, with the per-version availability table and the
BPFTRACE_BTFoverride - kprobes — the mechanism that needs no BTF, and is therefore the fallback for everything BTF cannot see
- Drawing Wire Formats with Mermaid Packet Diagrams — the house style for the
packet-betalayouts used above - Linux eBPF MOC — parent map (section 7, CO-RE and BTF)