feat(asm): unsuffixed mnemonics, mem-indirect jmp, endbr64/hlt/syscall

This commit is contained in:
Graeme Geldenhuys 2026-06-25 11:25:37 +01:00
parent fe7b016119
commit c673fc21f1
3 changed files with 155 additions and 14 deletions

View file

@ -163,6 +163,7 @@ type
Sym: string; { symbol name (for RIP-rel, labels, TLS) } Sym: string; { symbol name (for RIP-rel, labels, TLS) }
SymDisp: Int64; { addend to symbol (sym + N) } SymDisp: Int64; { addend to symbol (sym + N) }
TpOff: Boolean; { sym@tpoff — emit R_X86_64_TPOFF32 relocation } TpOff: Boolean; { sym@tpoff — emit R_X86_64_TPOFF32 relocation }
Indirect: Boolean; { memory operand reached via *mem (call/jmp *disp(%reg)) }
end; end;
{ Parsed line types } { Parsed line types }
@ -342,6 +343,7 @@ begin
Result.Scale := 1; Result.Scale := 1;
Result.Sym := ''; Result.Sym := '';
Result.SymDisp := 0; Result.SymDisp := 0;
Result.Indirect := False;
P := APos; P := APos;
while (P < Length(S)) and ((S[P]= Ord(' ')) or (S[P]= 9)) do while (P < Length(S)) and ((S[P]= Ord(' ')) or (S[P]= 9)) do
@ -353,7 +355,8 @@ begin
Exit; Exit;
end; end;
{ Indirect register: *%reg } { Indirect operand: *%reg (register-indirect) or *disp(%reg) (memory-
indirect), used by `call`/`jmp` through a pointer. }
if S[P]= Ord('*') then if S[P]= Ord('*') then
begin begin
P := P + 1; P := P + 1;
@ -371,6 +374,11 @@ begin
AEnd := P; AEnd := P;
Exit; Exit;
end; end;
{ *disp(%reg): parse the remainder as an ordinary memory operand and flag
it indirect so the branch encoder uses FF /4 with that memory ModRM. }
Result := ParseOperand(S, P, AEnd);
Result.Indirect := True;
Exit;
end; end;
{ Immediate: $N } { Immediate: $N }
@ -1244,6 +1252,45 @@ begin
Result := (AWidth = 8) and (ARegCode >= 4) and (ARegCode <= 7); Result := (AWidth = 8) and (ARegCode >= 4) and (ARegCode <= 7);
end; end;
{ Map an unsuffixed AT&T mnemonic (mov/lea/xor/…) to its size-suffixed form,
inferring the operand size from a register operand's width (GNU as does the
same). Mnemonics that already carry a suffix, and ones that are size-fixed in
long mode (push/pop/call/jmp are 64-bit), pass through. 64-bit is the default
when no register operand pins the width. }
function NormalizeMnemonic(const AMnem: string;
const AOp1, AOp2: TOperand): string;
var
W: Integer;
Suffix: string;
begin
Result := AMnem;
{ Width from whichever operand is a register (Op2 first — it is the
destination in AT&T two-operand forms). }
W := 0;
if AOp2.Kind = opReg then W := AOp2.RegW
else if AOp1.Kind = opReg then W := AOp1.RegW;
if W = 64 then Suffix := 'q'
else if W = 32 then Suffix := 'l'
else if W = 16 then Suffix := 'w'
else if W = 8 then Suffix := 'b'
else Suffix := 'q'; { default 64-bit when no register pins the size }
{ Size-inferred integer ALU/data mnemonics. }
if (AMnem = 'mov') or (AMnem = 'xor') or (AMnem = 'and') or (AMnem = 'or') or
(AMnem = 'add') or (AMnem = 'sub') or (AMnem = 'cmp') or (AMnem = 'test') or
(AMnem = 'inc') or (AMnem = 'dec') or (AMnem = 'neg') or (AMnem = 'not') or
(AMnem = 'imul') or (AMnem = 'shl') or (AMnem = 'shr') then
Result := AMnem + Suffix
{ lea's size is the destination pointer width — always 64-bit here. }
else if AMnem = 'lea' then
Result := 'leaq'
{ Stack/branch ops are 64-bit-only in long mode. }
else if AMnem = 'push' then Result := 'pushq'
else if AMnem = 'pop' then Result := 'popq'
else if AMnem = 'call' then Result := 'callq';
{ 'jmp' keeps its name (the encoder already handles jmp incl. indirect). }
end;
{ ---- Main instruction encoder ----------------------------------------- } { ---- Main instruction encoder ----------------------------------------- }
function EncodeInstruction(var ACtx: TEncodeContext; function EncodeInstruction(var ACtx: TEncodeContext;
@ -1261,9 +1308,10 @@ var
begin begin
CBInit(CB); CBInit(CB);
ACtx.ImmTail := 0; ACtx.ImmTail := 0;
Mnem := AParsed.Mnemonic;
Op1 := AParsed.Op1; Op1 := AParsed.Op1;
Op2 := AParsed.Op2; Op2 := AParsed.Op2;
{ Accept unsuffixed AT&T mnemonics (mov/lea/xor/…) by inferring the size. }
Mnem := NormalizeMnemonic(AParsed.Mnemonic, Op1, Op2);
{ ---- lock prefix (F0) — emitted ahead of the instruction it guards ---- } { ---- lock prefix (F0) — emitted ahead of the instruction it guards ---- }
if AParsed.HasLock then if AParsed.HasLock then
@ -1277,6 +1325,30 @@ begin
Exit; Exit;
end; end;
{ ---- endbr64 (CET) ---- F3 0F 1E FA }
if Mnem = 'endbr64' then
begin
CBEmit(CB, $F3); CBEmit(CB, $0F); CBEmit(CB, $1E); CBEmit(CB, $FA);
Result := CB.Data;
Exit;
end;
{ ---- hlt ---- F4 }
if Mnem = 'hlt' then
begin
CBEmit(CB, $F4);
Result := CB.Data;
Exit;
end;
{ ---- syscall ---- 0F 05 }
if Mnem = 'syscall' then
begin
CBEmit(CB, $0F); CBEmit(CB, $05);
Result := CB.Data;
Exit;
end;
{ ---- leave ---- } { ---- leave ---- }
if Mnem = 'leave' then if Mnem = 'leave' then
begin begin
@ -1433,10 +1505,25 @@ begin
end end
else if (Op1.Kind = opIndirect) then else if (Op1.Kind = opIndirect) then
begin begin
{ Register-indirect: jmp *%reg / call *%reg. FF /4 (jmp) or /2 (call). }
if Op1.Reg >= 8 then if Op1.Reg >= 8 then
CBEmit(CB, MakeRex(False, False, False, True)); CBEmit(CB, MakeRex(False, False, False, True));
CBEmit(CB, $FF); CBEmit(CB, $FF);
CBEmit(CB, MakeModRM(3, 4, Op1.Reg)); if Mnem = 'jmp' then
CBEmit(CB, MakeModRM(3, 4, Op1.Reg))
else
CBEmit(CB, MakeModRM(3, 2, Op1.Reg));
end
else if IsMemLike(Op1) and Op1.Indirect then
begin
{ Memory-indirect: jmp *disp(%reg) / call *disp(%reg). FF /4 or /2 with
the memory ModRM. }
EmitRexForMem(CB, False, 0, Op1, False);
CBEmit(CB, $FF);
if Mnem = 'jmp' then
EncodeMemOperand(CB, ACtx, Op1, 4)
else
EncodeMemOperand(CB, ACtx, Op1, 2);
end end
else else
raise EAssembler.Create(Mnem + ': unsupported operand'); raise EAssembler.Create(Mnem + ': unsupported operand');
@ -1546,22 +1633,24 @@ begin
end; end;
{ ---- incq / decq ---- } { ---- incq / decq ---- }
if (Mnem = 'incq') or (Mnem = 'decq') then if (Mnem = 'incq') or (Mnem = 'decq') or
(Mnem = 'incl') or (Mnem = 'decl') then
begin begin
{ FF /0 = inc, /1 = dec; REX.W only for the 'q' (64-bit) forms. }
if Op1.Kind = opReg then if Op1.Kind = opReg then
begin begin
EmitRexIfNeeded(CB, True, 0, Op1.Reg, False); EmitRexIfNeeded(CB, (Mnem = 'incq') or (Mnem = 'decq'), 0, Op1.Reg, False);
CBEmit(CB, $FF); CBEmit(CB, $FF);
if Mnem = 'incq' then if (Mnem = 'incq') or (Mnem = 'incl') then
CBEmit(CB, MakeModRM(3, 0, Op1.Reg)) CBEmit(CB, MakeModRM(3, 0, Op1.Reg))
else else
CBEmit(CB, MakeModRM(3, 1, Op1.Reg)); CBEmit(CB, MakeModRM(3, 1, Op1.Reg));
end end
else if IsMemLike(Op1) then else if IsMemLike(Op1) then
begin begin
EmitRexForMem(CB, True, 0, Op1, False); EmitRexForMem(CB, (Mnem = 'incq') or (Mnem = 'decq'), 0, Op1, False);
CBEmit(CB, $FF); CBEmit(CB, $FF);
if Mnem = 'incq' then if (Mnem = 'incq') or (Mnem = 'incl') then
EncodeMemOperand(CB, ACtx, Op1, 0) EncodeMemOperand(CB, ACtx, Op1, 0)
else else
EncodeMemOperand(CB, ACtx, Op1, 1); EncodeMemOperand(CB, ACtx, Op1, 1);

View file

@ -38,6 +38,10 @@ type
procedure TestMovqRegToXmm_SseEncoding; procedure TestMovqRegToXmm_SseEncoding;
procedure TestNegl_F7Slash3; procedure TestNegl_F7Slash3;
procedure TestLockXaddl_AtomicEncoding; procedure TestLockXaddl_AtomicEncoding;
procedure TestUnsuffixedMov_InfersSize;
procedure TestJmpIndirectMem_FFSlash4;
procedure TestEndbr64AndHlt;
procedure TestSyscall_0F05;
end; end;
{ ---- ELF writer unit tests ---- } { ---- ELF writer unit tests ---- }
@ -172,6 +176,52 @@ begin
ContainsBytes(Obj, Chr($F0) + Chr($0F) + Chr($C1) + Chr($07))); ContainsBytes(Obj, Chr($F0) + Chr($0F) + Chr($C1) + Chr($07)));
end; end;
procedure TAsmEncodingTests.TestUnsuffixedMov_InfersSize;
var
Obj: string;
begin
{ Unsuffixed AT&T mnemonics infer size from the register operand.
mov %rbx, (%rdi) is 64-bit -> 48 89 1f (same as movq). Verified vs cc. }
Obj := AssembleToBytes('mov %rbx, (%rdi)' + LineEnding);
AssertTrue('48 89 1f missing (mov inferred q)',
ContainsBytes(Obj, Chr($48) + Chr($89) + Chr($1F)));
{ xor %eax, %eax is 32-bit -> 31 c0 (no REX.W). }
Obj := AssembleToBytes('xor %eax, %eax' + LineEnding);
AssertTrue('31 c0 missing (xor inferred l)',
ContainsBytes(Obj, Chr($31) + Chr($C0)));
end;
procedure TAsmEncodingTests.TestJmpIndirectMem_FFSlash4;
var
Obj: string;
begin
{ jmp *56(%rdi) -> ff 67 38 (FF /4, memory-indirect). The longjmp tail. }
Obj := AssembleToBytes('jmp *56(%rdi)' + LineEnding);
AssertTrue('ff 67 38 missing',
ContainsBytes(Obj, Chr($FF) + Chr($67) + Chr($38)));
end;
procedure TAsmEncodingTests.TestEndbr64AndHlt;
var
Obj: string;
begin
{ endbr64 -> f3 0f 1e fa ; hlt -> f4 }
Obj := AssembleToBytes('endbr64' + LineEnding);
AssertTrue('f3 0f 1e fa missing',
ContainsBytes(Obj, Chr($F3) + Chr($0F) + Chr($1E) + Chr($FA)));
Obj := AssembleToBytes('hlt' + LineEnding);
AssertTrue('f4 missing', ContainsBytes(Obj, Chr($F4)));
end;
procedure TAsmEncodingTests.TestSyscall_0F05;
var
Obj: string;
begin
{ syscall -> 0f 05 (needed for the FreeBSD direct-syscall stubs). }
Obj := AssembleToBytes('syscall' + LineEnding);
AssertTrue('0f 05 missing', ContainsBytes(Obj, Chr($0F) + Chr($05)));
end;
procedure TAsmEncodingTests.TestQuadSymbol_EmitsReloc; procedure TAsmEncodingTests.TestQuadSymbol_EmitsReloc;
var var
Obj: string; Obj: string;

View file

@ -335,7 +335,7 @@ var
I: Integer; I: Integer;
M: TArchiveMember; M: TArchiveMember;
Obj: TElfObjectFile; Obj: TElfObjectFile;
SawSetjmp: Boolean; SawLongName: Boolean;
begin begin
RTLPath := ProjectRoot() + 'compiler/target/blaise_rtl.a'; RTLPath := ProjectRoot() + 'compiler/target/blaise_rtl.a';
if not FileExists(RTLPath) then if not FileExists(RTLPath) then
@ -348,12 +348,14 @@ begin
ReadArchiveFile(RTLPath, Members); ReadArchiveFile(RTLPath, Members);
AssertTrue('expected several RTL members, got ' AssertTrue('expected several RTL members, got '
+ IntToStr(Members.Count), Members.Count >= 5); + IntToStr(Members.Count), Members.Count >= 5);
SawSetjmp := False; { A member whose name exceeds 15 chars exercises the GNU long-name (//)
table. rtl_platform_layout_linux.o is the longest current RTL member. }
SawLongName := False;
for I := 0 to Members.Count - 1 do for I := 0 to Members.Count - 1 do
begin begin
M := Members.Get(I); M := Members.Get(I);
if M.Name = 'blaise_setjmp_x86_64.o' then if M.Name = 'rtl_platform_layout_linux.o' then
SawSetjmp := True; SawLongName := True;
{ Every member must parse as a valid x86-64 relocatable object { Every member must parse as a valid x86-64 relocatable object
with at least its NULL section + one real section. } with at least its NULL section + one real section. }
Obj := ParseElfObject(M.Data, M.Name); Obj := ParseElfObject(M.Data, M.Name);
@ -364,8 +366,8 @@ begin
Obj.Free(); Obj.Free();
end; end;
end; end;
AssertTrue('long-named member blaise_setjmp_x86_64.o not found ' AssertTrue('long-named member rtl_platform_layout_linux.o not found '
+ '(GNU long-name table mishandled?)', SawSetjmp); + '(GNU long-name table mishandled?)', SawLongName);
finally finally
for I := 0 to Members.Count - 1 do for I := 0 to Members.Count - 1 do
Members.Get(I).Free(); Members.Get(I).Free();