From eade875b5379a2e01ddebc70f74ab8b77c3223a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 24 Aug 2026 20:23:39 +0200 Subject: [PATCH] fix(asm): compress movq immediates to the go-tool-asm imm32 forms Assisted-by: GLM 5.3 --- asm/encode_test.go | 14 ++++++++++++-- asm/instrs.go | 25 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/asm/encode_test.go b/asm/encode_test.go index 0daf404..383776e 100644 --- a/asm/encode_test.go +++ b/asm/encode_test.go @@ -57,8 +57,11 @@ func TestMov(t *testing.T) { checkSyntax(t, "mov qword ptr [rbx], rax", "MOVQ", AX, Ptr(BX, 0, 8)) checkSyntax(t, "mov rbx, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), BX) checkSyntax(t, "mov rbx, qword ptr [rsi+4*rbx]", "MOVQ", Idx(SI, BX, 4, 0, 8), BX) - checkSyntax(t, "mov rax, 0x5", "MOVQ", Imm(5), AX) - checkSyntax(t, "mov r8, 0x5", "MOVQ", Imm(5), Reg{idx: 8, size: 8}) + // A small positive immediate compresses to the 32-bit zero-extending + // form (matching go tool asm), so the disassembler renders the 32-bit + // register name even for MOVQ. + checkSyntax(t, "mov eax, 0x5", "MOVQ", Imm(5), AX) + checkSyntax(t, "mov r8d, 0x5", "MOVQ", Imm(5), Reg{idx: 8, size: 8}) checkSyntax(t, "mov qword ptr [rax], 0x5", "MOVQ", Imm(5), Ptr(AX, 0, 8)) checkSyntax(t, "mov r12, r13", "MOVQ", Reg{idx: 13, size: 8}, Reg{idx: 12, size: 8}) } @@ -211,6 +214,13 @@ func TestScalarGroundTruth(t *testing.T) { {"BSRQ AX,CX", "BSRQ", []Operand{AX, CX}, "480fbdc8", "BSR"}, {"POPCNTL AX,CX", "POPCNTL", []Operand{AX, CX}, "f30fb8c8", "POPCNT"}, {"POPCNTQ R8,R9", "POPCNTQ", []Operand{r8, r9}, "f34d0fb8c8", "POPCNT"}, + // A 64-bit immediate that fits a signed int32 is compressed exactly + // as the Go assembler does: positive via B8+rd without REX.W + // (zero-extended), negative via REX.W C7 /0 (sign-extended). + {"MOVQ $4,BX", "MOVQ", []Operand{Imm(4), BX}, "bb04000000", "MOV"}, + {"MOVQ $4,R8", "MOVQ", []Operand{Imm(4), r8}, "41b804000000", "MOV"}, + {"MOVQ $-1,BX", "MOVQ", []Operand{Imm(-1), BX}, "48c7c3ffffffff", "MOV"}, + {"MOVQ big,BX", "MOVQ", []Operand{Imm(0x1122334455667788), BX}, "48bb8877665544332211", "MOV"}, {"CMOVLGT CX,AX", "CMOVLGT", []Operand{CX, AX}, "0f4fc1", "CMOVG"}, {"CMOVLEQ CX,AX", "CMOVLEQ", []Operand{CX, AX}, "0f44c1", "CMOVE"}, {"CMOVQGT R9,R8", "CMOVQGT", []Operand{r9, r8}, "4d0f4fc1", "CMOVG"}, diff --git a/asm/instrs.go b/asm/instrs.go index edd256b..d7e82cb 100644 --- a/asm/instrs.go +++ b/asm/instrs.go @@ -91,7 +91,28 @@ func (e *enc) encodeMov(ops []Operand, size int) error { case Imm: if dstIsReg { - // MOV r, imm: 0xB0+reg (8-bit) / 0xB8+reg (16/32/64, imm64 for Q). + v := int64(src) + // The Go assembler compresses 64-bit moves whose immediate fits + // a signed int32, choosing per sign: + // v >= 0: B8+rd imm32 without REX.W (zero-extended by the + // hardware, REX.B still emitted for R8-R15); + // v < 0: REX.W C7 /0 imm32 (sign-extended — the plain B8+rd + // form would zero-extend and corrupt the value). + // Out-of-range immediates keep the B8+rd imm64 form. + if size == 8 && v >= 0 && v <= (1<<31)-1 { + i := newInstr(4, []byte{0xB8 + byte(dstReg.idx&7)}) + i.rexB = dstReg.idx >= 8 + i.imm = le32(v) + return e.emit(i) + } + if size == 8 && v < 0 && v >= -(1<<31) { + i := newInstr(8, []byte{0xC7}) + if err := setRMDigit(i, 0, dstReg, 8); err != nil { + return err + } + i.imm = le32(v) + return e.emit(i) + } opBase := byte(0xB8) if size == 1 { opBase = 0xB0 @@ -101,7 +122,7 @@ func (e *enc) encodeMov(ops []Operand, size int) error { if dstReg.needsREX(size) { i.rexForced = true } - i.imm = immediate(int64(src), size, true) + i.imm = immediate(v, size, true) return e.emit(i) } // MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0.