fix(assembler): encode movq/movd between an XMM and a GP register (SSE MOVQ)
The internal assembler routed every `movq`/`movd` to the integer mov encoder,
which reads both operands as general-purpose registers. So `movq %xmm0, %rax`
was emitted as `mov %rax,%rax` (48 89 C0) — register 0 in both fields — silently
discarding the XMM value.
The native backend emits `movq %xmm0, %rax` to box a Double's raw bits into an
array-of-const (the Format() argument path) and to spill float bit patterns, so
ANY float passed to Format() under the internal assembler (now the default
toolchain) arrived as garbage and printed 0.0000. A regression vs v0.11.0,
which used the external assembler. The e2e test harness assembles native via
cc, so only the internal-assembler suite exercises this path.
Fix: `movq`/`movd` with an XMM operand is an SSE move and is routed to a new
EncodeMovqXmm:
gp/mem -> xmm : 66 [REX.W] 0F 6E /r
xmm -> gp/mem : 66 [REX.W] 0F 7E /r
with the XMM operand as the ModRM.reg field and REX.W set for movq (64-bit),
clear for movd (32-bit).
Tests:
- cp.test.assembler.pas TAsmEncodingTests: byte-level encoding of
`movq %xmm0,%rax` (66 48 0F 7E C0, and asserts it is NOT 48 89 C0) and
`movq %rax,%xmm0` (66 48 0F 6E C0).
- TInternalAsmE2ETests.TestFormatFloatArg: Format('%.4f',[1.25]) through the
internal assembler prints 1.2500.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3707 tests).
This commit is contained in:
parent
c5d49ba03e
commit
2a6d0c5912
|
|
@ -963,6 +963,8 @@ function EncodeMovExtend(var ACB: TCodeBuf; var ACtx: TEncodeContext;
|
|||
|
||||
function EncodeSSE(var ACB: TCodeBuf; var ACtx: TEncodeContext;
|
||||
const AMnem: string; const ASrc, ADst: TOperand): string; forward;
|
||||
function EncodeMovqXmm(var ACB: TCodeBuf; var ACtx: TEncodeContext;
|
||||
const AMnem: string; const ASrc, ADst: TOperand): string; forward;
|
||||
|
||||
{ Returns True if the mnemonic is a conditional or unconditional branch }
|
||||
function IsBranch(const AMnem: string): Boolean;
|
||||
|
|
@ -1454,6 +1456,17 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
{ ---- movq/movd between an XMM register and a GP register/memory ----
|
||||
`movq %xmm, %r64` and `movq %r64, %xmm` are SSE moves (66 [REX.W] 0F 7E/6E),
|
||||
NOT the integer reg-reg mov. Without this an `movq %xmm0, %rax` was encoded
|
||||
as `mov %rax,%rax` (both operands read as GP reg 0), silently dropping the
|
||||
xmm value — the cause of float args reaching Format() as garbage. }
|
||||
if ((Mnem = 'movq') or (Mnem = 'movd')) and (Op1.IsXmm or Op2.IsXmm) then
|
||||
begin
|
||||
Result := EncodeMovqXmm(CB, ACtx, Mnem, Op1, Op2);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
{ ---- movq/movl/movw/movb ---- }
|
||||
if (Mnem = 'movq') or (Mnem = 'movl') or (Mnem = 'movw')
|
||||
or (Mnem = 'movb') then
|
||||
|
|
@ -2238,6 +2251,35 @@ begin
|
|||
raise EAssembler.Create(AMnem + ': unsupported operand combination');
|
||||
end;
|
||||
|
||||
{ Encode `movq`/`movd` moving between an XMM register and a GP register or
|
||||
memory. The XMM operand is always the ModRM.reg field:
|
||||
xmm <- gp/mem : 66 [REX.W] 0F 6E /r (load into xmm)
|
||||
gp/mem <- xmm : 66 [REX.W] 0F 7E /r (store from xmm)
|
||||
REX.W is set for movq (64-bit), clear for movd (32-bit). This is the path the
|
||||
generic integer mov cannot encode (it would treat %xmm0 as %rax). }
|
||||
function EncodeMovqXmm(var ACB: TCodeBuf; var ACtx: TEncodeContext;
|
||||
const AMnem: string; const ASrc, ADst: TOperand): string;
|
||||
var
|
||||
NeedRexW: Boolean;
|
||||
begin
|
||||
NeedRexW := (AMnem = 'movq');
|
||||
if ADst.IsXmm and (ASrc.Kind = opReg) and not ASrc.IsXmm then
|
||||
{ gp -> xmm: opcode 6E, xmm is reg, gp is r/m. }
|
||||
EmitSSERegReg(ACB, $66, NeedRexW, $6E, -1, ASrc.Reg, ADst.Reg)
|
||||
else if ASrc.IsXmm and (ADst.Kind = opReg) and not ADst.IsXmm then
|
||||
{ xmm -> gp: opcode 7E, xmm is reg, gp is r/m. }
|
||||
EmitSSERegReg(ACB, $66, NeedRexW, $7E, -1, ADst.Reg, ASrc.Reg)
|
||||
else if ADst.IsXmm and IsMemLike(ASrc) then
|
||||
{ mem -> xmm: opcode 6E. }
|
||||
EmitSSERegMem(ACB, ACtx, $66, NeedRexW, $6E, -1, ADst.Reg, ASrc)
|
||||
else if ASrc.IsXmm and IsMemLike(ADst) then
|
||||
{ xmm -> mem: opcode 7E. }
|
||||
EmitSSERegMem(ACB, ACtx, $66, NeedRexW, $7E, -1, ASrc.Reg, ADst)
|
||||
else
|
||||
raise EAssembler.Create(AMnem + ': unsupported xmm/gp operand combination');
|
||||
Result := ACB.Data;
|
||||
end;
|
||||
|
||||
{ Re-raise an assembler error annotated with the source line number and
|
||||
raw line text. Without this, failures surface with zero diagnostics. }
|
||||
procedure AsmLineError(ALineNum: Integer; const ARawLine, AMsg: string);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ type
|
|||
procedure TestRexX_ExtendedIndexImmStore;
|
||||
procedure TestMovwImm16_TwoByteImmediate;
|
||||
procedure TestErrorMessage_HasLineNumber;
|
||||
procedure TestMovqXmmToReg_SseEncoding;
|
||||
procedure TestMovqRegToXmm_SseEncoding;
|
||||
end;
|
||||
|
||||
{ ---- ELF writer unit tests ---- }
|
||||
|
|
@ -94,6 +96,7 @@ type
|
|||
procedure TestNestedCalls;
|
||||
procedure TestClassVirtualCall;
|
||||
procedure TestFloatArithmetic;
|
||||
procedure TestFormatFloatArg;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -256,6 +259,31 @@ begin
|
|||
Pos('line 2', Msg) >= 0);
|
||||
end;
|
||||
|
||||
procedure TAsmEncodingTests.TestMovqXmmToReg_SseEncoding;
|
||||
var
|
||||
Obj: string;
|
||||
begin
|
||||
{ movq %xmm0, %rax -> 66 48 0F 7E C0 (SSE MOVQ store-from-xmm). This was
|
||||
mis-encoded as the integer `mov %rax,%rax` (48 89 C0), silently dropping the
|
||||
xmm value — the cause of float Format() args arriving as garbage. (issue
|
||||
#133 follow-up regression.) }
|
||||
Obj := AssembleToBytes('movq %xmm0, %rax' + LineEnding);
|
||||
AssertTrue('66 48 0F 7E C0 missing',
|
||||
ContainsBytes(Obj, Chr($66) + Chr($48) + Chr($0F) + Chr($7E) + Chr($C0)));
|
||||
AssertFalse('must NOT encode as integer mov %rax,%rax (48 89 C0)',
|
||||
ContainsBytes(Obj, Chr($48) + Chr($89) + Chr($C0)));
|
||||
end;
|
||||
|
||||
procedure TAsmEncodingTests.TestMovqRegToXmm_SseEncoding;
|
||||
var
|
||||
Obj: string;
|
||||
begin
|
||||
{ movq %rax, %xmm0 -> 66 48 0F 6E C0 (SSE MOVQ load-into-xmm). }
|
||||
Obj := AssembleToBytes('movq %rax, %xmm0' + LineEnding);
|
||||
AssertTrue('66 48 0F 6E C0 missing',
|
||||
ContainsBytes(Obj, Chr($66) + Chr($48) + Chr($0F) + Chr($6E) + Chr($C0)));
|
||||
end;
|
||||
|
||||
{ ---- ELF reader helpers ---- }
|
||||
|
||||
function TElfWriterTests.ReadU8(const ABuf: string; AOff: Integer): Integer;
|
||||
|
|
@ -935,6 +963,32 @@ begin
|
|||
AssertEquals('ok' + LineEnding, Out_);
|
||||
end;
|
||||
|
||||
procedure TInternalAsmE2ETests.TestFormatFloatArg;
|
||||
var
|
||||
Out_: string;
|
||||
EC: Integer;
|
||||
begin
|
||||
{ A float argument to Format() is boxed into the array-of-const via
|
||||
`movq %xmm0, %rax` (raw IEEE-754 bits). The internal assembler used to
|
||||
mis-encode that as `mov %rax,%rax`, so the value was dropped and the float
|
||||
printed as 0.0000. This is the end-to-end guard for that fix; the e2e test
|
||||
harness assembles native via cc, so only the internal-assembler suite
|
||||
exercises this path. (issue #133 follow-up regression.) }
|
||||
if not CompileAndRun(
|
||||
'program test_fmtfloat;' + LineEnding +
|
||||
'uses sysutils;' + LineEnding +
|
||||
'begin' + LineEnding +
|
||||
' WriteLn(Format(''%.4f'', [1.25]))' + LineEnding +
|
||||
'end.',
|
||||
Out_, EC) then
|
||||
begin
|
||||
Ignore('<toolchain-missing>');
|
||||
Exit;
|
||||
end;
|
||||
AssertEquals(0, EC);
|
||||
AssertEquals('1.2500' + LineEnding, Out_);
|
||||
end;
|
||||
|
||||
{ ---- Registration ---- }
|
||||
|
||||
initialization
|
||||
|
|
|
|||
Loading…
Reference in a new issue