Link-Time Dead Stripping: Preserve Registration Tables Without Keeping the Whole Program

A registration table can exist in an object file and still disappear from the final executable. Diagnose three separate gates: did the compiler emit the entry, did the linker select its object, and did section garbage collection retain the entry and its callback? A repair at one gate does not automatically satisfy the others.

This procedure uses GCC 14.2 and GNU BFD Binutils 2.44 on ELF. A small executed x86-64 fixture retained one callback registry with a targeted KEEP rule while allowing an unrelated function to be collected. No embedded linker layout or LTO build was tested.

Describe the registration mechanism precisely

A hypothetical component places a function pointer in a dedicated input section. Startup code discovers entries by walking the final registry range rather than naming every callback in ordinary C calls. The source therefore contains a real runtime requirement that may not look like an ordinary symbol-reference chain.

Write down the entry format, alignment, start/end boundary convention and callback lifetime. Include how many entries should exist for each feature configuration. Without an expected registry manifest, a program can boot successfully while silently losing an optional handler.

Use the final ELF and linker map to establish where entries ended up. A source annotation and a successful compile are evidence of intent. The outcome that matters is an iterable, correctly aligned registry containing the intended callbacks in the shipped artifact.

Three gates distinguish compiler emission, object selection and garbage-collection retention, with a tested KEEP rule preserving the registry-to-callback edge while unrelated code is removed.
Original three-gate model with executed registry-size and callback controls.

Gate one: compiler emission

The GCC 14.2 variable-attribute reference describes used as requiring emission of a static-storage variable that appears unreferenced. That addresses compiler omission. Do not infer from its name that used automatically defeats every later link-time removal.

Inspect the input object’s section table, symbol table and relocations. Confirm that the registry entry exists and that its function-pointer initializer refers to the expected callback. If the entry never reached the object, a linker KEEP pattern has nothing to retain.

For supported GNU or FreeBSD ELF OSABIs, GCC also documents retain, which protects against linker garbage collection and requires Binutils 2.36 or later. Treat it as a versioned alternative with explicit target support. This fixture used used plus a linker rule instead, so it does not claim an executed retain comparison.

Gate two: object selection

A registration-only member inside an archive may never be selected when no unresolved reference asks for it. Section retention and archive extraction are separate operations. Confirm the member appears among the link inputs before diagnosing the section’s fate.

A product can provide a deliberate entry anchor, list an object explicitly or use a narrowly bounded extraction policy for a dedicated registry archive. Each choice changes the build contract. Keep any –whole-archive use scoped and explain why every member is wanted; it is not a general substitute for understanding the registry.

The executed fixture listed its registration object directly, eliminating archive selection as a variable. This lets the comparison answer whether garbage collection removed an already-selected entry. An archive-based product needs an additional extraction test.

Gate three: section reachability

The Binutils 2.44 linker manual explains that –gc-sections follows roots and relocation references. A retained registry entry’s pointer can keep its callback reachable. If the registry entry itself has no retained path, both the entry and otherwise-unused callback can disappear.

Use –print-gc-sections and a map file to distinguish removal from placement. Check the exact input-section spelling, including suffixes generated by macros or language tooling. A rule matching .registry does not necessarily match entries emitted into .registry.entries.

For this controlled example, the targeted input rule was KEEP(*(.registry.entries)). It was placed within the output registry section of a small script fragment inserted after the ordinary data section. This is an illustrative host layout, not a complete firmware linker script to paste over a board’s memory map.

Interpret the executed comparison

The input object contained one used function-pointer entry, its callback returning 42 and an unrelated function returning 99. Both functions had independent sections. The consumer inspected the registry’s byte range and invoked the retained pointer only when its length matched the host pointer size.

Link case Observed result
Garbage collection disabled Registry occupied eight bytes; callback returned 42.
Garbage collection enabled without KEEP Registry range was empty despite used on the input entry.
Garbage collection enabled with targeted KEEP Eight-byte registry and callback survived; unrelated function was absent.

Eight bytes is the function-pointer size in this x86-64 fixture, not a portable entry-size constant. The range calculation used the tested ELF address representation. Target pointer representation, linker boundaries and iteration rules need their own reviewed implementation.

Avoid accidental retention that hides defects

A debug build, an extra direct callback call or broad export policy can make a registry appear healthy by introducing another root. Remove those accidental references from the negative control. Otherwise the test demonstrates that some path retained the callback, not that the registration mechanism is sufficient.

Boundary-symbol conventions can also affect reachability. If using linker-synthesized start/stop symbols or platform-specific section attributes, inspect the selected linker’s exact rules. The explicit boundary assignments in this fixture should not be generalized into a claim about every synthesized-symbol mode.

Make registry completeness a release gate

Compare expected entry identifiers and counts with the final artifact for enabled and disabled feature sets. Check that a retained entry points to the intended implementation, not merely to any executable address. Keep ordering requirements explicit; a table’s physical order should not accidentally become an undocumented initialization priority.

Repeat the proof after enabling LTO, changing section names or replacing the linker. The acceptance artifact is the input-to-output reachability map, a deliberate removal control and a targeted retention result. This preserves required behavior without disabling useful dead stripping for the entire program.

Similar Posts