Bug B — -O1 one-shot compile+link against an archive drops a local .Lkit_ro.N
Status: FIXED. The sqlite:O1 and yyjson:O1 builds now succeed; yyjson is
green end-to-end at both opt levels. (sqlite then trips a separate, previously
masked -O1 runtime fault — see sqlite-o1-runtime-segfault.md.)
Fix: src/link/link_resolve.c + src/link/link_reloc_layout.c. The
one-shot path links the just-compiled object in memory; a file-format emitter
runs obj_sweep_dead and omits removed (tombstoned) entries when it writes,
but the in-memory builder still holds them, and the linker walked them by raw
id without honoring the removed bit. A deferred-but-never-materialized local
rodata constant (obj_symbol_defer leaves .Lkit_ro.N a removed local SK_OBJ
when its function is optimized away at -O1) therefore survived as an undefined
local, and a dead reloc against one tripped "reloc references unmapped symbol".
The resolver now skips removed symbols and the reloc passes skip dead relocs
against them — matching exactly what a serialized-then-reread object presents.
Regression: test/driver/run.sh → cc-o1-oneshot-deferred-rodata.
Original report (kept for the root-cause record):
Component: linker / one-shot compile+link object handling at -O1
(src/link/, interaction with the in-invocation compiled object's local
.Lkit_ro rodata symbols).
Severity: kit cc main.c libfoo.a -o app at -O1 fails on real archives.
Symptom
fatal: link: undefined reference to '.Lkit_ro.119' # sqlite (shell.c + libsqlite3.a)
fatal: link: undefined reference to '.Lkit_ro.0' # yyjson (use_yyjson.c + libyyjson.a)
.Lkit_ro.N are kit's local (file-scope) read-only-data labels, numbered
from 0 per object. The reference is reported undefined even though the symbol
is defined locally in the archive member:
$ kit nm sqlite3.o | grep '.Lkit_ro.119'
0000000000007ad5 d .Lkit_ro.119 # 'd' = local data, present
Reproduce
make provision-ecosystem # once (network)
KIT=build/kit
SDK="$(xcrun --sdk macosx --show-sdk-path)"
SQ="$(sh scripts/ecosystem.sh srcdir sqlite)"
DEFS="-DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION"
$KIT cc -O1 --sysroot "$SDK" $DEFS -I "$SQ" -c "$SQ/sqlite3.c" -o /tmp/sqlite3.o
$KIT ar rcs /tmp/libsqlite3.a /tmp/sqlite3.o
# FAILS: one-shot — compile the driver source AND link the archive together:
$KIT cc -O1 --sysroot "$SDK" $DEFS -I "$SQ" "$SQ/shell.c" /tmp/libsqlite3.a -lc -o /tmp/sqlite3
# WORKS: pre-compile the driver to .o first, then link:
$KIT cc -O1 --sysroot "$SDK" $DEFS -I "$SQ" -c "$SQ/shell.c" -o /tmp/shell.o
$KIT cc -O1 --sysroot "$SDK" $DEFS /tmp/shell.o /tmp/libsqlite3.a -lc -o /tmp/sqlite3 # exit 0
What is and isn't affected
- -O0 one-shot links fine (sqlite is green at -O0 and matches clang).
- -O1 separate compile-then-link works (see the WORKS case above) — so the
archive member and its
.Lkit_rosymbols are individually sound. - The break is specific to the one-shot path: when
kit cccompiles a driver source in the same invocation as it links an archive at -O1. The just-compiled driver object also emits.Lkit_ro.Nnumbered from 0, so the leading suspicion is a collision between the in-invocation object's local.Lkit_ro.Nlabels and the archive member's, leaving one reference bound to a symbol that the resolver then can't find. - Related history: kit has had
.Lkit_ro/.Lkit_jtlocal-label globalize/resolution issues before (assembler tombstone-globalize for FreeBSD bootstrap). Worth checking whether one-shot link reuses local-symbol numbering state across the compiled-here object and the pulled-in archive member.
Notes for digging
- A tiny two-file repro (
a.cwith onestatic const char[],m.ccalling it, archived) did not reproduce — the trigger needs many.Lkit_rosymbols and/or specific numbering overlap, so reduce from sqlite/yyjson rather than from scratch. - The ecosystem harness links the driver one-shot on purpose; keeping that path is what holds this bug red.