-
Notifications
You must be signed in to change notification settings - Fork 0
Debug builds and split DWARF
For the flag itself — the three levels and what each costs at runtime — see
Documentation/usage.md.
This page covers the part that does not fit a reference: where the debug
information actually lives after --debug, and the one way to lose it.
A --debug build writes most of its DWARF into .dwo files that sit beside
the objects in packages/, not into the archives it installs and not into the
prefix. The installed prefix is therefore much smaller than a debug prefix used
to be — and it is no longer self-contained. Delete packages/ and the
binaries still run, but a debugger can no longer step into them.
If you only remember one thing: packages/ is load-bearing for as long as you
want to debug the prefix you installed from it.
Every debug level emits the same -g3. That was true before this change and is
still true: the levels trade optimization and assertions, never symbol depth
(lib/flags.sh, the level table). So an operator who wanted cheaper links had no
lever short of not using --debug at all — which is the one thing they had asked
for.
The prefix is built --disable-shared, so every consumer statically links every
archive, and every archive carried its full DWARF inside. Measured in
crippledgeek/rdlp, which links this prefix: 86 test binaries over 100 MB, many
near 1 GB each, and a target/debug of 94 GB. Peak RSS during parallel link
steps was enough to push a 15 GB machine into swap, because each concurrent
linker holds its own copy of that DWARF in memory.
-gsplit-dwarf moves the payload out of the object into a sibling .dwo and
leaves a skeleton behind. ar archives the object alone, so the .a a linker
reads collapses toward its non-debug size, while gdb still resolves the full
information through DW_AT_comp_dir + DW_AT_dwo_name.
Both columns are --debug=full builds of the same tree. "Inline" is the
measurement from #92;
"split" was measured on the installed prefix on 2026-09-03.
| Archive | Inline | Split | Factor |
|---|---|---|---|
libavcodec.a |
278 MB | 42.4 MiB | 6.6x |
libavformat.a |
124 MB | 12.2 MiB | 10.2x |
libavfilter.a |
116 MB | 16.3 MiB | 7.1x |
libavutil.a |
17 MB | 3.0 MiB | 5.7x |
Aggregate, same builds:
| Inline | Split | |
|---|---|---|
| All static archives in the prefix | 3.0 GB | 810 MB |
| Installed prefix total | 5.1 GB | 2.4 GB |
The debug information did not shrink — it moved. It now lives in 12,930 .dwo
files totalling 1.57 GiB under packages/, which is why the next section
matters.
The ratio holds at every level rather than only at -O0. Measured on real
libavcodec sources with this tree's own configured FFmpeg CFLAGS (gcc 16.2.1),
23 of the largest objects: 12896 KiB → 4332 KiB at -O0 -g3 (3.0x), and
8796 KiB → 2484 KiB at -O2 -g3 (3.5x). symbols benefits as much as full.
The clearest way to see what the split is worth is to turn it off. lame built four ways with the exact flags each level composes (2026-09-03):
| Build | libmp3lame.a |
vs a normal build |
|---|---|---|
normal, no --debug
|
0.49 MiB | 1.00x |
--debug=symbols |
1.06 MiB | 2.16x |
--debug=balanced |
0.97 MiB | 1.99x |
--debug=full |
0.84 MiB | 1.72x |
--debug=full, -gsplit-dwarf removed |
5.39 MiB | 11.01x |
Two things there are the opposite of what people expect. full is the
smallest of the three levels, because -O0 emits smaller code than -O2 and,
past the split, the -O choice moves the archive more than the debug info does.
And the archive is small only because the debug info is elsewhere — 30 .dwo
files totalling 1.6 MiB, for this one small library.
A debugger that cannot find the .dwo files does not degrade gracefully.
Driven, not assumed: gdb reports
Could not find DWO CU <path>.dwo
and then places no breakpoint in that compilation unit. The binary still links, still runs, and behaves identically. Only the debugger notices, and it notices one CU at a time.
Three ways to lose them:
-
mediaforge.sh cleanremoves the build trees,.dwoincluded.lib/cleanup.shwarns before it does. -
Copying the prefix to another machine without
packages/. The prefix is not portable on its own — this is the known gap tracked in #94. -
Rebuilding a recipe. New objects, new
.dwo; anything still linked against the old archive is now looking for files that were replaced.
The skeleton left in the object is what keeps this from being total: a backtrace still resolves function names. It is source-level stepping, locals and macros that go.
# The binary should still say "with debug_info, not stripped"
file ~/.local/mediaforge/bin/ffmpeg
# The .dwo population that backs it
find packages -name '*.dwo' | wc -l
# Source-level debugging against the installed prefix
gdb --batch -ex 'break av_packet_alloc' -ex 'info breakpoints' \
~/.local/mediaforge/bin/ffmpegA breakpoint resolving to libavcodec/packet.c means the .dwo lookup is
working. Could not find DWO CU means it is not — check that packages/ still
holds the tree the prefix was built from.
-g3 survives the move: info macro still answers in a binary linked against a
split archive, so the macro payload that makes -g3 worth having over -g2 is
intact.
macOS — a no-op rather than a hazard. Splitting is an ELF feature; clang for
a Mach-O target accepts the flag, writes no .dwo, and emits a byte-identical
object (measured, clang 22.1.8, -target arm64-apple-darwin). Nothing to
retain, nothing to lose.
ccache — handled. A cache hit restores the .dwo beside the object
(measured, ccache 4.13.6). This matters because lib/ccache.sh puts ccache in
front of every compile it can.
cmake — the split survives cmake's flag ordering. cmake appends
CMAKE_<LANG>_FLAGS_<CONFIG> after CMAKE_<LANG>_FLAGS, so the compile line
really does read -g3 -gsplit-dwarf ... -g -DNDEBUG. A trailing plain -g does
not downgrade -g3, and -gsplit-dwarf is not a -g level at all, so nothing
cmake appends turns it off. Measured on a macro-defining probe.
Rust and CUDA are not covered. rav1e (cargo) and nv-codec (nvcc) read no
CFLAGS and each spell this differently — cargo's is split-debuginfo, nvcc's
is different again. Both still ship their DWARF inside their objects. This is
recorded rather than discovered: if your link is still heavy, those two are why.
-
Documentation/usage.md— the--debugflag reference - #92 — the measurement and the change
- #94 — making the split optional, for a portable prefix