feat(asm): strip @PLT from symbol names; probe stale fallback compiler in e2e tests

This commit is contained in:
Graeme Geldenhuys 2026-06-25 12:03:09 +01:00
parent 821ce90caf
commit 7781242696
3 changed files with 94 additions and 5 deletions

View file

@ -543,12 +543,14 @@ begin
end
else
begin
while (P < Length(S)) and (IsAlnum(S[P]) or (S[P]= Ord('.'))
or (S[P]= Ord('_')) or (S[P]= Ord('$')) or (S[P]= Ord('@'))) do
begin
SymName := SymName + Chr(S[P]);
{ Any other @SUFFIX (e.g. @PLT, @GOTPCREL) is a relocation qualifier,
NOT part of the symbol name consume and discard it. @PLT just
selects the PLT32 relocation, which call/jmp already emit by default.
Appending it to the name would create a bogus symbol like
`__libc_start_main@PLT` that no loader resolves. }
P := P + 1;
while (P < Length(S)) and (IsAlnum(S[P]) or (S[P]= Ord('_'))) do
P := P + 1;
end;
end;
end;

View file

@ -42,6 +42,7 @@ type
procedure TestJmpIndirectMem_FFSlash4;
procedure TestEndbr64AndHlt;
procedure TestSyscall_0F05;
procedure TestCallPltStripsSuffix;
end;
{ ---- ELF writer unit tests ---- }
@ -126,6 +127,14 @@ implementation
var
GInternalAsmSkipNoted: Boolean = False;
{ Validity-probe cache for the fallback compiler see the matching note in
cp.test.cli.pas. The /tmp/fp_blaise2 fallback is a transient fixpoint
artifact that is often stale; a stale binary turns these e2e tests into a
cascade of cryptic failures. Probe once and skip (not fail) on a bad probe.
0 = not probed, 1 = good, 2 = bad. }
var
GInternalAsmProbeState: Integer = 0;
{ ---- TAsmEncodingTests ---- }
function TAsmEncodingTests.ContainsBytes(const ABuf, APat: string): Boolean;
@ -222,6 +231,20 @@ begin
AssertTrue('0f 05 missing', ContainsBytes(Obj, Chr($0F) + Chr($05)));
end;
procedure TAsmEncodingTests.TestCallPltStripsSuffix;
var
Obj: string;
begin
{ call foo@PLT must reference symbol 'foo' (with a PLT32 reloc), NOT a bogus
symbol literally named 'foo@PLT' that no loader resolves. The @PLT suffix
is a relocation qualifier, not part of the name. Regression for the
_start migration (call __libc_start_main@PLT). }
Obj := AssembleToBytes('call foo@PLT' + LineEnding);
AssertTrue('symbol foo missing', ContainsBytes(Obj, 'foo'));
AssertTrue('bogus foo@PLT symbol present',
not ContainsBytes(Obj, 'foo@PLT'));
end;
procedure TAsmEncodingTests.TestQuadSymbol_EmitsReloc;
var
Obj: string;
@ -672,6 +695,9 @@ begin
end;
function TInternalAsmE2ETests.CompilerAvailable: Boolean;
var
SrcFile, OutFile, CompOut, RunOut: string;
Rc: Integer;
begin
Result := FileExists(FCompiler) and FileExists(FRTL);
if (not Result) and (not GInternalAsmSkipNoted) then
@ -680,7 +706,34 @@ begin
WriteLn(StdErr, 'note: TInternalAsmE2ETests skipped — compiler binary "',
FCompiler, '" or RTL "', FRTL, '" not found ',
'(set BLAISE_QBE_COMPILER to a QBE-backend blaise binary to run them)');
Exit;
end;
if not Result then Exit;
{ Validity probe: compile+run a trivial program with --assembler internal.
A stale fallback binary compiles fine but produces a SIGILL/wrong-output
executable; detect that here and skip rather than emit a cryptic cascade. }
if GInternalAsmProbeState = 0 then
begin
SrcFile := FScratch + 'probe.pas';
OutFile := FScratch + 'probe';
WriteFile(SrcFile, 'program p; begin WriteLn(42); end.');
Rc := Self.RunProc(FCompiler, [
'--source', SrcFile, '--unit-path', FRTLPath, '--unit-path', FStdlibPath,
'--output', OutFile, '--backend', 'native', '--assembler', 'internal'
], CompOut);
if (Rc = 0) and (Self.RunProcNoArgs(OutFile, RunOut) = 0)
and (Pos('42', RunOut) >= 0) then
GInternalAsmProbeState := 1
else
begin
GInternalAsmProbeState := 2;
WriteLn(StdErr, 'note: TInternalAsmE2ETests skipped — compiler binary "',
FCompiler, '" is stale/broken (probe program did not run); ',
'rebuild it or set BLAISE_QBE_COMPILER to a current binary.');
end;
end;
Result := GInternalAsmProbeState = 1;
end;
function TInternalAsmE2ETests.RunProc(const AExe: string;

View file

@ -94,6 +94,17 @@ implementation
var
GCLISkipNoted: Boolean = False;
{ Validity-probe cache for the fallback compiler. The fallback path
(/tmp/fp_blaise2) is a transient fixpoint artifact that is frequently STALE
built by an earlier, possibly-broken compiler. Running the contract tests
against a stale binary produces a cascade of cryptic failures (e.g. SIGILL
from a since-fixed mis-encoding) that look like regressions but are not.
We probe the binary once (compile+run a trivial program) and, if it does not
behave, skip the suite with an actionable message instead of failing.
0 = not probed, 1 = good, 2 = bad. }
var
GCLIProbeState: Integer = 0;
{ ---- helpers ---- }
function TCLIContractTests.ProjectRoot: string;
@ -140,6 +151,9 @@ begin
end;
function TCLIContractTests.CompilerAvailable: Boolean;
var
Out_: string;
EC: Integer;
begin
Result := FileExists(FCompiler) and FileExists(FRTL);
if (not Result) and (not GCLISkipNoted) then
@ -148,7 +162,27 @@ begin
WriteLn(StdErr, 'note: TCLIContractTests skipped — compiler binary "',
FCompiler, '" or RTL "', FRTL, '" not found ',
'(set BLAISE_QBE_COMPILER to a QBE-backend blaise binary to run them)');
Exit;
end;
if not Result then Exit;
{ Validity probe: a stale fallback binary would otherwise turn into a cascade
of cryptic failures. Probe once; on a bad probe, treat as unavailable. }
if GCLIProbeState = 0 then
begin
if CompileRunFull('program p; begin WriteLn(42); end.', 'native', Out_, EC)
and (EC = 0) and (Pos('42', Out_) >= 0) then
GCLIProbeState := 1
else
begin
GCLIProbeState := 2;
WriteLn(StdErr, 'note: TCLIContractTests skipped — compiler binary "',
FCompiler, '" is stale/broken (probe program did not run); ',
'rebuild it or set BLAISE_QBE_COMPILER to a current QBE-backend ',
'blaise binary.');
end;
end;
Result := GCLIProbeState = 1;
end;
function TCLIContractTests.RunCompiler(const AArgs: array of string;