Skip to content

Comments

Support BPF object linking#1942

Merged
ti-mo merged 7 commits intomainfrom
feature/load-linked-elfs
Feb 18, 2026
Merged

Support BPF object linking#1942
ti-mo merged 7 commits intomainfrom
feature/load-linked-elfs

Conversation

@dylandreimerink
Copy link
Member

Lets add support for ELF object linking, that is loading ELF files that have been linked.

In normal non-eBPF C programs it is very normal to have multiple compilation units (C files that compile to .o files) and then at the end to have a separate linker combine these into an executable. In the eBPF world we typically work with single compilation units which we turn into CollectionSpec's and then Collections.

However, libbpf does offer linker logic which can be used via the bpftool utility. You invoke it like bpftool gen object out.o in1.o in2.o in3.o ..... In this linking process the contents of the sections are simply appended to each other, and the linker, and the linker keeps only the symbols that "win". The rule being that a strong symbol overwrites a weak symbol, so a strong symbol from in3.o will overwrite a weak symbol from in1.o. If multiple weak symbols are present then the first takes precedence. So a weak symbol in in1.o wins over one in in3.o.

So, a practical example is our new testdata added in the PR, this is linker1.o (edited to only show .text symbols):

llvm-objdump -j .text -t -s linker1-el.elf

linker1-el.elf: file format elf64-bpf

SYMBOL TABLE:
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000  w    F .text  0000000000000010 fun_l1w
0000000000000010 g     F .text  0000000000000010 fun_l1s
0000000000000020  w    F .text  0000000000000010 fun_ww
0000000000000030 g     F .text  0000000000000010 fun_l1os
0000000000000040  w    F .text  0000000000000010 fun_l1ow
0000000000000000         *UND*  0000000000000000 fun_l2os
Contents of section .text:
 0000 b7000000 01000000 95000000 00000000  ................
 0010 b7000000 02000000 95000000 00000000  ................
 0020 b7000000 03000000 95000000 00000000  ................
 0030 b7000000 04000000 95000000 00000000  ................
 0040 b7000000 05000000 95000000 00000000  ................

And here is linker2.o

llvm-objdump -j .text -t -s linker2-el.elf

linker2-el.elf: file format elf64-bpf

SYMBOL TABLE:
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 g     F .text  0000000000000010 fun_l1w
0000000000000010  w    F .text  0000000000000010 fun_l1s
0000000000000020  w    F .text  0000000000000010 fun_ww
0000000000000030 g     F .text  0000000000000010 fun_l2os
0000000000000040  w    F .text  0000000000000010 fun_l2ow
0000000000000000         *UND*  0000000000000000 fun_l1os
Contents of section .text:
 0000 b7000000 06000000 95000000 00000000  ................
 0010 b7000000 07000000 95000000 00000000  ................
 0020 b7000000 08000000 95000000 00000000  ................
 0030 b7000000 09000000 95000000 00000000  ................
 0040 b7000000 0a000000 95000000 00000000  ................

This is the linked object:

llvm-objdump -j .text -t -s linked-el.elf 

linked-el.elf:  file format elf64-bpf

SYMBOL TABLE:
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000050 g     F .text  0000000000000010 fun_l1w
0000000000000010 g     F .text  0000000000000010 fun_l1s
0000000000000020  w    F .text  0000000000000010 fun_ww
0000000000000030 g     F .text  0000000000000010 fun_l1os
0000000000000040  w    F .text  0000000000000010 fun_l1ow
0000000000000080 g     F .text  0000000000000010 fun_l2os
0000000000000090  w    F .text  0000000000000010 fun_l2ow
Contents of section .text:
 0000 b7000000 01000000 95000000 00000000  ................
 0010 b7000000 02000000 95000000 00000000  ................
 0020 b7000000 03000000 95000000 00000000  ................
 0030 b7000000 04000000 95000000 00000000  ................
 0040 b7000000 05000000 95000000 00000000  ................
 0050 b7000000 06000000 95000000 00000000  ................
 0060 b7000000 07000000 95000000 00000000  ................
 0070 b7000000 08000000 95000000 00000000  ................
 0080 b7000000 09000000 95000000 00000000  ................
 0090 b7000000 0a000000 95000000 00000000  ................

Here you can see, specifically from the second column of the .text dump that both sections are combined. Crucially for the assumptions of the library, is that not the whole contents of the section should be used. We need to collect all symbols for a given section, and only consider the bits of the section indicated by a symbol (offset ; offset+size). The same is also true for maps.

Fixes: #466

@dylandreimerink dylandreimerink force-pushed the feature/load-linked-elfs branch 8 times, most recently from c5aba68 to 3b71af2 Compare January 26, 2026 15:47
@dylandreimerink dylandreimerink marked this pull request as ready for review January 29, 2026 09:13
@dylandreimerink dylandreimerink requested a review from a team as a code owner January 29, 2026 09:13
ti-mo and others added 7 commits February 18, 2026 13:26
…n metadata

Also removed ExtInfos.Assign and AssignMetadataToInstructions, their functionality
was moved to the root package.

The *Offsets types were converted to type aliases to make them passable to generic
functions like take(). As discrete types, they would need explicit conversions,
making them awkward to work with.

Signed-off-by: Timo Beckers <[email protected]>
This was only opening the -el.elf file.

Signed-off-by: Timo Beckers <[email protected]>
Programs (functions) can be defined as weak, allowing the program text
to be overwritten by a non-weak candidate during a linking step. During
linking, the linker will simply append all affected symbols' function
bodies. The resulting ELF will contain both the original and the new
instructions. However, the symbol table will only contain an entry for
the 'winning' symbol. The bytes between the end of the symbol and the
next symbol can be safely ignored.

Up until now, we've made the assumption that whole sections should
always be translated into Instructions. However, this will result in
unnecessary Instructions being decoded in linked objects, and requires
us to split the Instructions into symbols without knowledge of the
original symbol size in the ELF.

Instead of decoding in multiple stages, decode one symbol at a time and
stop processing bytecode when the end of the symbol has been reached.
This avoids unnecessary work and saves on allocations when decoding
linked objects.

Signed-off-by: Timo Beckers <[email protected]>
Suggested-by: Dylan Reimerink <[email protected]>
Similar to the previous commit, legacy struct bpf_map_def map definitions
are appended in the 'maps' ELF section, but only the winning one receives
a symbol. This commit uses the BufferedSectionReader and only reads exactly
what's mentioned in the symbol table.

Some redundant btf.Var conversions/lookups were removed, along with some
BTF sanity checks we've never seen fail. The library's job isn't strictly
to scrutinize the ELF and BTF. The only checks that were kept exist to
reduce the likelyhood of out-of-bounds reads.

Signed-off-by: Timo Beckers <[email protected]>
Suggested-by: Dylan Reimerink <[email protected]>
The ELF reader was updated to support per-symbol instruction decoding in
a previous commit. This commit removes the guardrails that were supposed
to prevent the user from loading invalid program sections if they
contained multiple candidates for a function symbol. Loading weak
functions is now allowed.

Signed-off-by: Dylan Reimerink <[email protected]>
This commit adds testdata for ELF linking. `linked1.c` and `linked2.c`
contain strong and weak symbols and are linked together using `bpftool
gen object`.

This commit also updates the toolchain image to include `bpftool`.

Signed-off-by: Dylan Reimerink <[email protected]>
Co-authored-by: Timo Beckers <[email protected]>
Test whether all maps in the ELF have a MaxEntries of 1 after linking,
and whether all programs return 0 when running them after linking.

Also adds a new testutils.SkipNonNativeEndian to skip a test when a given
endianness doesn't match the host's endianness.

Signed-off-by: Timo Beckers <[email protected]>
@ti-mo ti-mo force-pushed the feature/load-linked-elfs branch from 3b71af2 to e928b3d Compare February 18, 2026 12:26
@github-actions github-actions bot added the breaking-change Changes exported API label Feb 18, 2026
@ti-mo
Copy link
Contributor

ti-mo commented Feb 18, 2026

Quick bench on the new instruction unmarshaler code:

goos: linux
goarch: amd64
pkg: github.com/cilium/ebpf
cpu: AMD Ryzen 7 3700X 8-Core Processor
                           │   old.txt   │           new.txt            │
                           │   sec/op    │   sec/op     vs base         │
LoadCollectionManyProgs-16   565.2µ ± 2%   560.1µ ± 2%  ~ (p=0.132 n=6)

                           │   old.txt    │               new.txt               │
                           │     B/op     │     B/op      vs base               │
LoadCollectionManyProgs-16   621.7Ki ± 0%   480.3Ki ± 0%  -22.75% (p=0.002 n=6)

                           │   old.txt   │              new.txt              │
                           │  allocs/op  │  allocs/op   vs base              │
LoadCollectionManyProgs-16   4.996k ± 0%   5.087k ± 0%  +1.82% (p=0.002 n=6)

Alloc totals are down a significant amount, but LoadCollectionManyProgs is a pathological example. At least good to know we haven't regressed there. 🙂

@ti-mo ti-mo merged commit 3609a64 into main Feb 18, 2026
19 checks passed
@ti-mo ti-mo deleted the feature/load-linked-elfs branch February 18, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Changes exported API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

elf: support bpf object linking (bpftool gen object)

2 participants