Field guide

Comparing two rooms by hand

A visitor's guide — no agent, no MCP, just two browser tabs and the URL bar.

The question

Both of these programs do the same thing: they run and return 0. One is a teaching demo (build/hello, a one-line printf); the other is /usr/bin/true, the coreutils tool whose entire job is to exit successfully. Yet on disk:

file sizeprovenance
hello16,984 bytes (0x4258)exhibit-001-elf-atlas/fixtures/hello.jsonfile.size
/usr/bin/true43,248 bytes (0xa8f0)…/rooms/true/true.jsonfile.size

true is 2.5× the size. Why?

The one-line spoiler: true's executable code alone — its .text section, 19,027 bytes — is larger than the entire hello file (16,984). The gap isn't packaging or padding. It's that true is a real, well-behaved, user-facing tool and hello is a demo. Let's watch that happen, section by section.

You'll drive both rooms by hand. Every configuration of a room lives in the URL hash (#v=1&view=…&scale=…&select=…&tab=…), so each step below gives you two clickable links that land the two tabs on the same kind of view. Open them side by side.


Step 1 — The whole file, honestly scaled

Set both tabs to atlas view at true scale, where every structure's height is strictly proportional to its byte count.

What you see: two columns of very different length. In hello, no single band dominates — the file is mostly tables, padding, and DWARF debug info. In true, one band swallows the column: .text.

Now open the section header table in each — it's an array, and its length is the section count:

The first surprise: hello has 37 sections; true has only 28. The bigger file has fewer sections. That's because hello is an unstripped teaching build carrying seven .debug_* DWARF sections, while true is a stripped distro binary — its debug info was split out (you'll find a lone .gnu_debuglink pointing at a separate file). So the size delta is not debug metadata. Hold that thought.


Step 2 — The code mass (.text)

Open the executable segment (Segment 2, R-X) in each and disassemble .text:

What you see: hello's .text is 275 bytes — 68 rows in the Disasm tab, and you can scroll the whole function past in a second. main is at 0x1139; five instructions later it calls puts and returns. That's the program.

true's .text is 19,027 bytes — 4,637 rows. You can scroll for a long time. This is not one function; it's argument parsing, --help/--version handling, locale setup, and the coreutils "close stdout and check for write errors" shutdown dance — all compiled in.

What it means: 19,027 > 16,984. true's code by itself outweighs the entire hello file. Everything from here just explains what all that code is for.

(Provenance: .text sizes from each fixture's sections[]; row counts are the Disasm-tab rows in disassembly[".text"] — 68 and 4,637 respectively.)


Step 3 — What the program has to say (Strings tab)

Open .rodata's strings in each:

What you see: hello's .rodata is 17 bytes and holds exactly one string — "hello, world" at offset 0x2004 (no trailing newline; puts adds it). That's the whole read-only payload.

true's .rodata is 2,927 bytes with 46 strings, and reading them tells you what the program is: --help, --version, GNU coreutils, https://www.gnu.org/software/coreutils/, bug-coreutils@gnu.org, license and "Written by" boilerplate. None of this executes; all of it exists because a real tool must be able to describe itself to a human.

What it means: the difference between a demo and a tool starts showing up in the data, not just the code — true carries a small manual.


Step 4 — Who they call (library chips + foreign drawer)

Open the libc.so.6 overview in each — the list of libc functions the binary actually imports:

What you see: hello imports 3 libc functions (exhibit-001-elf-atlas/fixtures/imports.libc.json): puts, __libc_start_main, and __cxa_finalize. Two of those are pure runtime plumbing (process start-up and at-exit teardown) — the only call hello makes to do its actual job is puts.

true imports 46 (…/rooms/true/imports.libc.json). Skim them and the size story explains itself. The tell-tale groups:

The nice twist: click through to any function body — say puts — and note the provenance banner. Both rooms resolve to the same library: glibc 2.43+r22+g8362e8ce10b2-2, build-id 020d6f7c33b2413f4fe10814c4729dce1387f049. So the difference isn't a different libc. It's how much of that one libc each program reaches into.


Step 5 — How they bind (optional, for the curious)

The two even resolve their imports differently. Open the binding machinery:

What you see: hello has a .plt (a 6-row stub table with a puts@plt entry) plus .rela.plt and .got.plt — classic lazy binding, where puts's real address is patched in on first call. Its DT_FLAGS_1 is 0x8000000 (DF_1_PIE only).

true has no .plt at all — look at its section list, there's no .plt, no .rela.plt, no .got.plt. Instead its .dynamic carries DT_FLAGS = 8 (DF_BIND_NOW) and DT_FLAGS_1 = 0x8000001 (DF_1_PIE | DF_1_NOW): every symbol is resolved eagerly at load time, and its relocations are packed into the compact .relr.dyn table you've just selected.

What it means: true is built with the full-RELRO / BIND-NOW hardening a distributed system binary gets; hello is built plain. A structural difference, not a size one — but it's why the two even look different in the relocation sections.


The payoff

Line the two .text bars up at true scale (Step 1) one more time. The entire argument fits in that picture: true is bigger because being a correct, user-facing Unix tool is not free. It costs a .text full of option parsing, an i18n subsystem, --help/--version prose, write-error checking at exit, and compiler hardening — 44 extra libc calls' worth of behavior that a printf("hello, world") demo never needs. hello's size, meanwhile, is dominated by fixtures for teaching (debug sections, tables, padding), not by its 275 bytes of code.

And yet, at the end of the day, both programs just return 0.


Verify it yourself

Every number above is literally in the fixture JSON — you don't have to take the docent's word for it.

  curl -s -H 'User-Agent: helloscope-doc' \
    https://helloscope-live.iunknown.workers.dev/rooms/true/true.json > true.json

Then: .file.size → 43248; .sections | length → 28; the .text entry's .size → 19027; .disassembly[".text"] | length → 4637; .strings[".rodata"] | length → 46. (jq reads all of these.)

If a caption ever disagrees with the JSON, believe the JSON.