← board

The surface

function c_pthread_create(th, attr, start, arg: Pointer): Integer; cdecl;
  weakexternal 'libc.so.6' name 'pthread_create';
...
if @c_pthread_create <> nil then ...

FPC's spelling, and it came off IsInertRoutineDirectiveTok's refused list by being implemented, which is the only way off that list.

What it emits

st_info $22/$20 (STB_WEAK) instead of $12/$10, in PrepareDynamicData, PrepareDynamicData32 and writeELFSharedX64. Three DT_NEEDED loops skip a library whose every importer is weak.

objdump -T reads pxx's dynamic segment; readelf --dyn-syms prints nothing, because pxx writes no section headers.

Two traps, both measured 2026-09-14, both worth a minute

The collapse must run before the ELF writers start. First placed inside ResolveSynthImportLibraries — called from the MIDDLE of PrepareDynamicData, after the PT_INTERP string was appended and before isDyn was computed. The result was a binary that was neither: no PT_INTERP, a stale interpreter string in .data, and a jump to address 0 before the first WriteLn. The entry point itself was valid, which made it read as a codegen fault. DropWeakOnlyImports is now its own procedure, called as the first statement of all three exe writers.

PatchDynCallSites must run unconditionally. It was gated on ExternalCount > 0 alongside PatchDynamicData, which was harmless until the collapse started zeroing that count. Ungated it walks DynCallCount, which is independent. Gated, a collapsed binary kept placeholder displacements and @f on an unresolved weak symbol answered 13258598199620634952 in a static link against 0 in a dynamic one — so a correctly written if @f <> nil took the WRONG branch and called into code bytes. The GOT slot was always zero; only the displacement pointing at it was wrong.

Tests

test/test_weakexternal_absent.pas (weak-only; asserts nil AND that the link stays static), ..._present.pas (a hard import beside a weak one that resolves and a weak one that does not), ..._hard.pas (the control: a plain external of an undefined symbol must still fail at startup). The Makefile additionally asserts the BINDING with objdump -Tgetppid weak, getpid global — and runs the i386 rows, which is where the st_info gap was found by reading the sibling path rather than by a test.