You have a function that nothing calls. A linker with --gc-sections will delete it, and most people take that as an optimisation they can have or not have. It is more interesting than that: the decision is a graph walk, and the graph is drawn by the optimiser, not by you.
One source file, compiled four ways:
$ cat gc.c <<'EOF'
#include <stdio.h>
int used(int x){ return x + 1; }
int unused(int x){ return x * 999; }
int also_unused(int x){ return unused(x) * 2; }
int main(void){ printf("%d\n", used(41)); return 0; }
EOF
$ for opt in -O1 -O0; do
clang $opt -ffunction-sections -c gc.c -o gc$opt.o
clang $opt gc$opt.o -o gc$opt.off
clang $opt -Wl,--gc-sections gc$opt.o -o gc$opt.on
printf " %s input sections: " $opt
readelf -SW gc$opt.o | sed -n 's/^ *\[[ 0-9]*\] \(\.text\.[a-z_]*\).*/\1 /p'
for m in off on; do
printf " --gc-sections %-3s .text=%s funcs: " $m \
"$(readelf -SW gc$opt.$m|awk '/ \.text /{print $6}')"
readelf -sW gc$opt.$m | awk '$4=="FUNC" && $7!="UND" && $8!~/^(_start|_init|_fini|frame_dummy|register_tm|deregister_tm|__do_global)/{printf "%s ", $8}'
echo
done
done
Read the two “on” rows against each other:
-O1 on .text=000108 funcs: main
-O0 on .text=000131 funcs: used main
^^^^
at -O1 the collector removed `used`.
at -O0 it KEPT it.
Same source. Same flag. Same linker. The only difference is the optimisation level, and the only thing that changed is whether the compiler inlined used into main.
The algorithm, in full. It is four steps and about forty lines of real linker code.
Step 4 is the one with teeth: the relocation is removed too, not just the target. That is why the removal is safe in a way that deleting a function by hand is not — if anything still pointed at the deleted code, the pointer would already have marked it live.
Now the two words that decide everything, and they come from different files:
And neither of them is a command-line flag in the normal case — they are lines in the 276-line file the reader has never opened. A constructor that nothing calls survives garbage collection because of a word inside parentheses in a script. Delete the KEEP and it is gone; add --gc-sections and you find out.
The inlining, shown at the instruction level, because this is the part that is not obvious:
$ objdump -d gc-O1.off --section=.text | sed -n '/<main>:/,/ret/p'
0000000000001170 <main>:
1170: 50 push %rax
1171: 48 8d 3d 8c 0e 00 00 lea 0xe8c(%rip),%rdi
1178: be 2a 00 00 00 mov $0x2a,%esi
^^^^^^^^^^^^^^^^
0x2a = 42 = used(41)
117d: 31 c0 xor %eax,%eax
117f: e8 ac fe ff ff call 1030 <printf@plt>
1184: 31 c0 xor %eax,%eax
1186: 59 pop %rcx
1187: c3 ret
mov $0x2a,%esi. That is used(41) computed at compile time. The out-of-line used in .text.used is still in the object file, still correct, and now has no caller anywhere. The collector did not decide it was dead code. It decided nothing points at it.
And at -O0 the same function is genuinely called, so it genuinely is live:
-O1 off .text=000138 funcs: used unused main also_unused -O1 on .text=000108 funcs: main (-0x30) -O0 off .text=000161 funcs: used unused main also_unused -O0 on .text=000131 funcs: used main (-0x30)
Both collect exactly 0x30 bytes and it is the same 0x30 — the two dead functions in both cases. The difference is entirely whether one more function was on the list. --gc-sections is a reachability walk and reachability is a property of the graph the compiler handed over, not of the source you wrote.
Now the root, moved. ENTRY is the only thing making main reachable, so change it:
$ grep -n '^ENTRY' default.ld
8:ENTRY(_start)
$ clang -O1 -Wl,--gc-sections -Wl,-e,used gc-O1.o -o gc.root
$ readelf -sW gc.root | awk '$4=="FUNC" && $7!="UND"{printf "%s ", $8}'
... used ...
$ echo " main is GONE."
One flag, and the survivor set inverts.used is now the root so it obviously survives; main is now unreachable so it is deleted. The program still links, the binary is smaller, and it does nothing anyone asked for. Nothing warns you that you just removed main from your program — -e used is a perfectly reasonable thing to type.
And KEEP, which is the other half. A constructor is called by nobody:
$ cat ctor.c <<'EOF'
#include <stdio.h>
__attribute__((constructor)) static void c1(void){ puts("c1"); }
int main(void){ puts("main"); return 0; }
EOF
$ clang -O1 -ffunction-sections -c ctor.c -o ctor.o
$ clang -O1 -o ctor.off ctor.o
$ clang -O1 -Wl,--gc-sections -o ctor.on ctor.o
$ for m in off on; do
printf " --gc-sections %-3s funcs: " $m
readelf -sW ctor.$m | awk '$4=="FUNC" && $7!="UND" && $8!~/^(_start|_init|_fini|frame_dummy|register_tm|deregister_tm|__do_global)/{printf "%s ", $8}'
echo
done
--gc-sections off funcs: c1 main
--gc-sections on funcs: c1 main <-- c1 SURVIVES
$ objdump -d ctor.on | grep -c 'call.*<c1>'
0 <-- and NOTHING calls it
c1 survives garbage collection and nothing in the program calls it. The only reason it is in the binary is a line in the default script:
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
}
Nothing in the program's control flow reaches c1. The C runtime reaches it by walking a table of function pointers, and the only thing the linker knows about that table is that a rule said KEEP. KEEP exists precisely for this: it is how you tell a reachability walk about a reference the linker cannot see, because the reference is data, not a relocation.
The failure this whole mechanism is defending against, because it is the classic linker-script bug and it is silent.
/* a perfectly ordinary C++ file */
static int table[16]; /* .data.rel.ro.local */
int lookup(int i) { return table[i & 15]; }
/* a header-only thing, compiled into ONE .o */
/* with -fdata-sections, so its table is: */
__attribute__((section(".mystuff"))) int mine[4] = {1,2,3,4};
int get_mine(int i){ return mine[i & 3]; }
link with -Wl,--gc-sections and your OWN script, which
does not mention .mystuff.
get_mine() is called -> get_mine survives.
mine[] is in a section the script never named.
if .mystuff is NOT an orphan that got placed, and is
not matched by any wildcard, it is DISCARDED.
result: get_mine() reads whatever is at that address.
no error. no warning. the program returns garbage.
That is the bug, and the fix is one word. Either give the section a rule that KEEPs it, or add KEEP(*(.mystuff)) to an existing rule, or use KEEP (*(.mystuff)) in your own script. The --gc-sections flag is what made the bug possible, and the KEEP is what makes it survivable.
Which raises the question every linker-script author asks: how do I know which sections need KEEP? And there is a real answer, and it is not “all of them”:
That is the principle, and it generalises past the list.KEEP is needed exactly where a reference is encoded as a value in an array rather than as a relocation. A vtable, a plugin registry, a jump table of function addresses, a constructor list — all invisible to the walk, all needing KEEP. And note that the default script KEEPs every one of the list above: it is not being careful, it is being correct, and it was written by people whose programs broke without it.
One more measured consequence, because it is a nice illustration that --gc-sections is not free. The collected sections leave gaps in the symbol table, and the symbol table is a large fraction of a small binary:
s_dyn (dynamic, no gc):
000348 .symtab
0001c9 .strtab
s_static (-static, libc linked in):
000348 -> 00c210 .symtab <-- 0xc210, 48 KB of symbols
08497d .text
01c57c .rodata
00966c .eh_frame
That is the next concept’s territory, and the honest summary of the trade is this: --gc-sections makes your code smaller and your binary honest about what it uses, at the cost of a build step whose output depends on the optimisation level in a way that is not obvious from the source. A function can be live in the source and absent from the binary, and the compiler decides.
$ cd courses/link/assets/samples $ ./build_samples.sh 2>&2 | sed -n '/F10/,/F11/p' $ python3 crosscheck.py 2>&2 | sed -n '/\\[10\\]/,/\\[11\\]/p' $ python3 linklab.py gc
Then build the graph yourself and check your prediction against it:
1. Write a function whose ONLY caller is another
function that is itself unreachable. Does it
survive? (Predict before running. The answer is
the interesting one: reachability is not
transitive backwards from roots, it is transitive
FORWARD from them, and a dead island is dead.)
2. Put a function pointer in a global array and call
through the array. Does it survive --gc-sections?
If not, what is the smallest script change that
saves it? (This is the .init_array case by
hand, and it is the exercise that teaches the
principle.)
3. Add a .text.unlikely function to gc.c and re-run
at -O1. Is it kept? (Rule 1 of the .text block is
.text.unlikely -- does rule ORDER affect survival,
or only position?)
4. Take the ctor.c case and remove the KEEP from
.init_array in your own copy of the script. Now
c1 is collected. Does the link warn? Does the
program still run? Does it still print "c1"?
5. Compile gc.c WITHOUT -ffunction-sections and with
--gc-sections. How much is collected now? (This is
why the flag is nearly useless without
-ffunction-sections, and the reason is worth being
able to state.)
Exercise 2 is the one that matters, and it is the closest thing to a real bug you can build in five minutes. A function called only through a table is invisible to the walk, and the program compiles, links, and returns wrong answers. If you have ever had a plugin registry, a vtable, or a callback table mysteriously stop working after a linker flag change, this is the mechanism, and you now have the five-word fix.
Exercise 5 has the answer that makes the flag’s real requirement explicit. --gc-sections collects sections, not functions. Without -ffunction-sections the compiler puts every function in one .text, that one section is live because anything in it is live, and the flag does nothing at all. The two flags are a pair: one creates the granularity, the other exploits it. That is not documented as a dependency anywhere obvious and it is the reason a build can add --gc-sections for a size win and measure no change.
This is the first selection concept and it is the answer to the question the language concept planted. KEEP was listed there as one of five operators and deferred with “this is where it earns its keep”. It earns it here, and it earns it by being the only thing standing between a constructor and deletion. The previous two module's concepts were about where things go; this one is about whether they are there at all, which is a bigger difference than it sounds.
The connection to the linker-defined symbols concept is exact and worth spelling out. That concept introduced __start_INIT_ARRAY and __stop_INIT_ARRAY and asked where they come from. The answer, visible twice on this page, is the two lines in the same script block as the KEEP:
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) ...))
PROVIDE_HIDDEN (__init_array_end = .);
^ ^
the symbols a C startup file walks the reason the
from one address to the other contents survive
The same three lines define the symbols and protect the data, and a program that only wanted the symbols would break if you deleted the KEEP. That is a design where one line is load-bearing for a reason that has nothing to do with the line above it, and it is the clearest example in the course of why reading a script end to end is worth the twenty minutes.
The strongest connection is to the AArch64 relocation pair in the Relocations course, and it is the same lesson twice. There, a tool author was told a relocation list must be processed as a set with an ordering constraint, because a half-applied ADRP/ADD pair is still a valid instruction pointing at the wrong place. Here, a tool author is told a section list must be processed as a graph with a fixpoint, because processing sections independently gives a plausible file rather than an error. Both failures are silent, both are caused by treating a structure with dependencies as a flat list, and both are the reason “read the format properly” is a correctness requirement rather than a style preference.
Forward within the module, the question of which sections exist at all is the natural next step. The Sections the Script Forgot is about sections the script never names — and its measurement is that even the 276-line default script orphans five of them in a trivial hello-world, four of them from the C runtime. The relationship between the two concepts is exact: --gc-sections removes what is unreachable, orphan handling places what was never mentioned, and the default script needs the second mechanism precisely because the first exists. A script that named every section would not need orphan handling; a script that garbage-collects must rely on the fallback for anything it forgot.
One connection outward, to the static-linking module. What -static Actually Does measures 0xc210 bytes of .symtab in a statically linked binary, against 0x348 in a dynamic one. Garbage collection is the tool that addresses exactly that — it removes the code those symbols describe. But as this page’s last measurement shows, it is not free: it depends on -ffunction-sections, and its output depends on the optimisation level in a way that is not visible in the source. That trade, measured rather than asserted, is what the next two concepts put numbers on.