Exhibit · article form
true.c is 80 lines — so why is the binary 43 KB?
A guided tour through /usr/bin/true (rebuilt with symbols), captured from the ELF Atlas exhibit (custom tour: tour.json) — 8 steps.
true is the program that does nothing — it returns success and exits. Its source, coreutils src/true.c, is about 80 lines. Yet the shipped /usr/bin/true is ~43 KB, and the file on screen is larger still: build/true.debug is 109,648 bytes, with a 20,275-byte .text.
One caveat before we start: this is not the shipped binary byte-for-byte. It is the same coreutils true source rebuilt in a frozen toolchain with -g (debug info kept), so it carries a different build-id. Strip it and it collapses to about 44 KB — within roughly 1% of the shipped /usr/bin/true's 43,248 bytes (Exhibit 002, where that figure is on the page) — same source, same tools. The point of keeping the debug info is that every one of those 20,275 .text bytes now maps back to a real source file. Full DWARF; the symbol table names 158 functions. Let's use that to find out what actually fills the file.
Here is the program itself. In the Disasm tab the machine code is grouped under the DWARF source lines it came from, and main — 282 bytes at 0x2cfb, all mapped to src/true.c — is almost embarrassingly small.
Its real logic is three instructions. At 0x2d0a cmpl $0x2,-0x4(%rbp) (source true.c:59, if (argc == 2)) asks whether there is exactly one argument. In the common case there is not, so 0x2d0e jne 2e0e jumps straight past everything to 0x2e0e mov $0x0,%eax (true.c:79, return EXIT_STATUS), then 0x2e13 leave / 0x2e14 ret (true.c:80). That is the whole program: set the return value to 0 and return.
The only other path is the argc == 2 branch — and notice it contains no "work" either, just the ritual every well-behaved GNU tool performs. 0x2d1e call set_program_name, then 0x2d32 call setlocale@plt, 0x2d4b call bindtextdomain@plt, 0x2d5a call textdomain@plt (true.c:62–65) — locale and translation setup. 0x2d69 call atexit (true.c:69) registers a cleanup handler, close_stdout. Hold onto that one.
Then two string compares decide the tool's contract: 0x2d86 call streq … 0x2d94 call usage handles --help (true.c:71–72), and 0x2db1 call streq … 0x2e09 call version_etc handles --version (true.c:74–75). Even true's "features" are just --help and --version.
So where did 20 KB of code come from? The DWARF map answers by source file. Scroll the Disasm tab and the ── file:line ── headers keep changing — this one .text section is stitched together from 27 different source files.
Count the address ranges each file owns (from the DWARF line map, 1,693 ranges total): lib/quotearg.c 699, lib/version-etc.c 166, src/system.h 156, lib/mbrtoc32.c 140, lib/xmalloc.c 137, lib/error.c 74. And src/true.c — the actual program — just 32, under 2% of the file. The other 98% is gnulib: the shared "model GNU command" scaffolding for argument quoting, --version text, Unicode/locale handling, and checked allocation. The stripped shipped binary has all this mass too; it just can't tell you any of these names.
Take one piece up close. When main handles --version it calls version_etc at 0x2e09 (true.c:75); that function lives at 0x5ee6, is 224 bytes, and every row of it maps to lib/version-etc.c — its prologue at 0x5ee6 sits under version-etc.c:233.
That is 166 ranges of machine code whose entire job is to print the author line, copyright, and GPL notice you see from true --version. true.c never spells any of that out; it just calls one gnulib function and inherits the standard GNU --version block. (The biggest file of all, lib/quotearg.c at 699 ranges, is the argument-quoting layer pulled in with gnulib's error machinery — code true's own logic never obviously touches.)
Follow the program off its own edge. When true finishes — whether usage calls exit(status) directly (0x2cf6 call 22a0 <exit@plt>, true.c:51) or main just returns 0 to __libc_start_main — control lands in libc's exit. The drawer's "Outside this file" banner marks the crossing: this is a reference copy of exit from libc.so.6, glibc 2.43+r22 (build-id 020d6f7c…), symbol version GLIBC_2.2.5, recovered from stdlib/exit.c.
It is tiny: 30 bytes, 7 rows, at 0x411c0 in the library's own address space. exit is the shared success path every one of the 47 imported libc functions ultimately sits behind.
Here is exit in C, recovered from stdlib/exit.c. The whole body is a single statement — line 148: __run_exit_handlers (status, &__exit_funcs, true, true); — and it compiles to exit's one and only call, 0x411d9 call 40f10.
An honesty note about that call: the archive's own control-flow analysis reports the target 0x40f10 as landing inside no exported function — it claims no name for it. So __run_exit_handlers is the name the source gives; the symbol table does not confirm it. What matters for true is what those handlers do: remember the atexit(close_stdout) registered back at true.c:69? This is where it runs — flushing standard output on the way out, so a true piped into something still delivers cleanly. exit reaches line 148, the handlers fire, and the process is gone.
So: an 80-line program, a 43 KB shipped binary. The gap is not the program — src/true.c is 32 of 1,693 mapped ranges. The gap is the contract of being a proper GNU tool: internationalized messages, a --help and --version block, quoted arguments in errors, checked allocation — the gnulib scaffolding in lib/quotearg.c (699), lib/version-etc.c (166), lib/mbrtoc32.c (140), lib/xmalloc.c (137), and the rest.
The shipped /usr/bin/true carries every byte of this too, but stripped it can only gesture at it. Because this copy was rebuilt from real source with its DWARF intact, we could name each piece by the file it came from — the honest, legible answer to "why is true 43 KB?"