Exhibit · article form

One hello.c, two builds: what -g -O0 and -O2 (stripped) really differ by

Captured from the side-by-side story of the two builds — the 17,024-byte debug (-g -O0) vs the 14,472-byte release (-O2, stripped) — 9 steps.

Step 1 / 9

One hello.c, two builds, side by side. The left pane is the debug build — -g -O0, 17,024 bytes. The right is the release build — -O2, stripped, 14,472 bytes. Same six-line source, same output, 2,552 bytes apart. Scroll to walk the differences — every number is in the bytes on screen.

Both builds side by side at step 1
Step 2 / 9

Start at the inventory. The debug file carries 37 sections; the release file 29. Highlighted is the section header table — the file's own table of contents. It sits at offset 0x3940 in debug but 0x3148 in release: everything after the stripped sections slides down. And release's 29 are a strict subset of debug's 37 — stripping only removes; it invents nothing.

Both builds side by side at step 2
Step 3 / 9

.symtab864 bytes, 36 symbols: the name table that lets a debugger say main instead of 0x1139. Select it and the release pane comes up empty — it exists only in debug; that is the note above the panes talking. strip deleted exactly this.

Both builds side by side at step 3
Step 4 / 9

Right behind it, .strtab486 bytes of the actual name strings .symtab points into. Also debug-only. Together the symbol machinery costs 1,350 bytes — and buys every human-readable name a debugger shows you.

Both builds side by side at step 4
Step 5 / 9

The DWARF group: six .debug_* sections, 594 bytes in all, only in the debug build. Highlighted is .debug_line86 bytes mapping instructions back to hello.c lines. This is why a debugger can step through source. The release build cut the wire.

Both builds side by side at step 5
Step 6 / 9

Now something identical in both panes: .dynsym168 bytes, 7 dynamic symbols — with .dynstr at 141 bytes. What the dynamic linker needs at runtime survives stripping untouched. Strip removes what humans read, never what the loader reads.

Both builds side by side at step 6
Step 7 / 9

The twist. Debug .text: 275 bytes. Release .text: 281 bytes — the optimized build's code is 6 bytes bigger, and the entry point moves from 0x1040 to 0x1060. -O2 is not a compressor; it trades bytes for speed where it likes. The savings live elsewhere.

Both builds side by side at step 7
Step 8 / 9

So where did 2,552 bytes go? 1,944 in stripped sections — 864 + 486 of symbols, 594 of DWARF. 512 in the section header table itself — eight fewer entries at 64 bytes each. The remainder is the shrunken section-name table and alignment slack. Nothing else moved: both files load with the same four PT_LOAD segments.

Both builds side by side at step 8
Step 9 / 9

Same program, same segments, same hello, world. One strip apart: 17,024 → 14,472 bytes — names and line-maps gone, loader untouched. Now poke at it yourself: click any section in either pane, or open the free-explore room from the link under the title.

Both builds side by side at step 9