--dce under --emit-obj crashes a two-object i386 link before main
Measured
At 1e9990bdb070, converged after 1 round(s):
| build | i386 | x86-64 |
|---|---|---|
--no-dce --emit-obj |
rc=0, 20 11 |
rc=0, 20 11 |
--dce --emit-obj |
rc=138, no output | rc=0, 20 11 |
./compiler/pascal26 --emit-obj --dce --target=i386 test/c_obj_fnptr_a.c a.o
./compiler/pascal26 --emit-obj --dce --target=i386 test/c_obj_fnptr_b.c b.o
gcc -m32 -no-pie a.o b.o -o f386 # links cleanly
./f386 # rc=138, prints nothing
make test-emit-obj reports it as expect_same: MISMATCH [fnp_386], expected
20 11, actual empty, with User defined signal 1 on the line above.
Why it was not found before
The pass has been allowed under -c since 2026-09-02 and the emit-obj rows
assert plenty about it — on x86-64, and on a single object. This fixture is
the only two-object i386 link in the tier, and it runs without --dce today, so
nothing exercised the combination. The link SUCCEEDS, which is the expensive
shape: no undefined reference, no diagnostic, a crash before main.
What it is not
Not a regression and not caused by any default. --dce is opt-in on this path
today and reproduces with the flag spelled explicitly. It was found by turning
the default on experimentally; that default was reverted, and this ticket is
the reason.
The lead
dce.inc's own note is the place to start: the two tables it did not originally
compact are the init/fini thunk offsets, stated raw in .rela.init_array /
.rela.fini_array, and "turning the refusal off without them SEGFAULTS BEFORE
main" — which is exactly the observed symptom, one target over. The x86-64 arm
handles them; the question is whether the i386 object writer states those
offsets somewhere the pass does not re-patch. Two objects matter because each
carries its own init/fini array, so the second object's thunks are the ones that
have to survive a compaction they did not participate in.
Not measured: whether one object alone with a non-empty .init_array reproduces
it, which would separate "i386 thunk offsets" from "two objects".
Re-measured 2026-09-21 (frankb-8e), compiler sha256 5dd0316573b3
STILL REPRODUCES, and deterministically — 5/5 runs. Confirmed live at HEAD; nothing here is a repair. Two corrections to the record above, both carried rather than substituted, because a number whose population was not written down is not refuted by a new one.
| build | i386 | x86-64 |
|---|---|---|
--no-dce --emit-obj |
rc=0, 20 11 |
rc=0, 20 11 |
--dce --emit-obj |
rc=139, no output | rc=0, 20 11 |
- The rc is 139 here, not 138.
139 = 128+11 = SIGSEGV; the recorded138 = 128+10 = SIGUSR1, which is what "User defined signal 1" above names. Both readings were taken on a two-objectgcc -m32 -no-pielink of the same two fixtures. I have not established whether the signal drifted or whether the original was produced under a different link; do not treat either number as the expected value — the assertable fact is "dies before main with no output", which both agree on and which a rc-specific expectation would make brittle. --target=x86-64IS NOT A VALID FLAG — the compiler answersunknown option: --target=x86-64. x86-64 is the DEFAULT and the row above is produced by passing no--targetat all. The original x86-64 rows are correct as results; they were not produced by the spelling implied here, and a reader copying that spelling gets a compile failure and may read it as the crash.
Population, so the next re-run has a denominator: two objects,
test/c_obj_fnptr_a.c and test/c_obj_fnptr_b.c, linked with
gcc [-m32] -no-pie, run 5 times each, on this box at HEAD.
Log
- 2026-09-21 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 30898eef7.