fix(amd64): correct guard displacements, frameless FP offsets and immediate ranges

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-19 23:49:07 +02:00
parent 94e09e8070
commit 4258131a3a
8 changed files with 377 additions and 74 deletions
+86 -21
View File
@@ -173,7 +173,11 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
if dstReg.needsREX(size) {
i.rexForced = true
}
i.imm = immediate(v, size, true)
imm, err := immediate(v, size, true)
if err != nil {
return err
}
i.imm = imm
return e.emit(i)
}
// MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0.
@@ -185,7 +189,11 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
if err := setRMDigit(i, 0, dst, size); err != nil {
return err
}
i.imm = immediate(int64(src), size, false)
imm, err := immediate(int64(src), size, false)
if err != nil {
return err
}
i.imm = imm
return e.emit(i)
}
return fmt.Errorf("MOV: invalid operands")
@@ -297,11 +305,15 @@ func (e *enc) encodeALU(op struct {
func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
if size == 1 {
immBytes, err := immediate(imm, 1, false)
if err != nil {
return err
}
i := newInstr(1, []byte{0x80})
if err := setRMDigit(i, digit, dst, 1); err != nil {
return err
}
i.imm = []byte{byte(int8(imm))}
i.imm = immBytes
return e.emit(i)
}
if fits8(imm) {
@@ -319,7 +331,11 @@ func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
if r, ok := dst.(Reg); ok && r.idx == 0 {
accOp := map[int]byte{0: 0x05, 1: 0x0D, 2: 0x15, 3: 0x1D, 4: 0x25, 5: 0x2D, 6: 0x35, 7: 0x3D}[digit]
i := newInstr(size, []byte{accOp})
i.imm = immediate(imm, size, false)
immBytes, err := immediate(imm, size, false)
if err != nil {
return err
}
i.imm = immBytes
return e.emit(i)
}
// 0x81 /digit, imm16/imm32.
@@ -327,7 +343,11 @@ func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
if err := setRMDigit(i, digit, dst, size); err != nil {
return err
}
i.imm = immediate(imm, size, false)
immBytes, err := immediate(imm, size, false)
if err != nil {
return err
}
i.imm = immBytes
return e.emit(i)
}
@@ -348,7 +368,11 @@ func (e *enc) encodeTest(ops []Operand, size int) error {
op = 0xA8
}
i := newInstr(size, []byte{op})
i.imm = immediate(int64(imm), size, false)
immBytes, err := immediate(int64(imm), size, false)
if err != nil {
return err
}
i.imm = immBytes
return e.emit(i)
}
op := byte(0xF7)
@@ -359,7 +383,11 @@ func (e *enc) encodeTest(ops []Operand, size int) error {
if err := setRMDigit(i, 0, dst, size); err != nil {
return err
}
i.imm = immediate(int64(imm), size, false)
immBytes, err := immediate(int64(imm), size, false)
if err != nil {
return err
}
i.imm = immBytes
return e.emit(i)
}
srcReg, ok := src.(Reg)
@@ -457,7 +485,13 @@ func (e *enc) encodeShift(digit int, ops []Operand, size int) error {
}
return e.emit(i)
}
// 0xC0 (8-bit) / 0xC1, imm8.
// 0xC0 (8-bit) / 0xC1, imm8. The count is an unsigned byte: go tool asm
// rejects negative and ≥256 counts, and the hardware masks the count, so
// a silent truncation ($300 encoding 44) would shift by a different
// amount than the source states.
if imm < 0 || imm > 255 {
return fmt.Errorf("shift count $%d is out of the 0..255 range", int64(imm))
}
op := byte(0xC1)
if size == 1 {
op = 0xC0
@@ -466,7 +500,7 @@ func (e *enc) encodeShift(digit int, ops []Operand, size int) error {
if err := setRMDigit(i, digit, dst, size); err != nil {
return err
}
i.imm = []byte{byte(int8(imm))}
i.imm = []byte{byte(imm)}
return e.emit(i)
}
@@ -508,7 +542,11 @@ func (e *enc) encodeImul(ops []Operand, size int) error {
if err := setRM(i, dstReg, ops[1], size); err != nil {
return err
}
i.imm = immediate(int64(imm), size, false)
immBytes, err := immediate(int64(imm), size, false)
if err != nil {
return err
}
i.imm = immBytes
return e.emit(i)
}
return fmt.Errorf("IMUL expects 2 or 3 operands, got %d", len(ops))
@@ -516,10 +554,21 @@ func (e *enc) encodeImul(ops []Operand, size int) error {
// --- PUSH / POP -------------------------------------------------------------
func (e *enc) encodePushPop(ops []Operand, push bool) error {
func (e *enc) encodePushPop(ops []Operand, size int, push bool) error {
if len(ops) != 1 {
return fmt.Errorf("PUSH/POP expects 1 operand, got %d", len(ops))
}
// In 64-bit mode go tool asm knows the 64-bit push (the default, with or
// without the Q suffix) and the 16-bit W form with its 0x66 operand-size
// prefix, and rejects the B and L spellings outright ("illegal in 64-bit
// mode"); silently widening those would push a different width than the
// source states.
switch size {
case 0, 8, 2:
default:
return fmt.Errorf("PUSH/POP size suffix is illegal in 64-bit mode")
}
w16 := size == 2
switch op := ops[0].(type) {
case Reg:
base := byte(0x50) // PUSH r; POP is 0x58
@@ -527,7 +576,7 @@ func (e *enc) encodePushPop(ops []Operand, push bool) error {
base = 0x58
}
// PUSH/POP default to 64-bit in 64-bit mode; no REX.W needed.
i := &instr{opcode: []byte{base + byte(op.idx&7)}, modrm: -1, sib: -1}
i := &instr{opSize16: w16, opcode: []byte{base + byte(op.idx&7)}, modrm: -1, sib: -1}
i.rexB = op.idx >= 8
return e.emit(i)
case Mem:
@@ -537,7 +586,7 @@ func (e *enc) encodePushPop(ops []Operand, push bool) error {
opc = 0x8F // POP r/m: /0
digit = 0
}
i := &instr{opcode: []byte{opc}, modrm: -1, sib: -1}
i := &instr{opSize16: w16, opcode: []byte{opc}, modrm: -1, sib: -1}
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
return err
}
@@ -547,10 +596,17 @@ func (e *enc) encodePushPop(ops []Operand, push bool) error {
return fmt.Errorf("POP does not take an immediate")
}
if fits8(int64(op)) {
i := &instr{opcode: []byte{0x6A}, modrm: -1, sib: -1, imm: []byte{byte(int8(op))}}
i := &instr{opSize16: w16, opcode: []byte{0x6A}, modrm: -1, sib: -1, imm: []byte{byte(int8(op))}}
return e.emit(i)
}
i := &instr{opSize16: false, opcode: []byte{0x68}, modrm: -1, sib: -1, imm: le32(int64(op))}
// PUSH imm32, sign-extended to 64 bits; go tool asm bounds the
// immediate by the same signed/unsigned 32-bit span as every other
// scalar immediate.
immBytes, err := immediate(int64(op), 8, false)
if err != nil {
return err
}
i := &instr{opSize16: w16, opcode: []byte{0x68}, modrm: -1, sib: -1, imm: immBytes}
return e.emit(i)
}
return fmt.Errorf("PUSH/POP: invalid operand")
@@ -639,19 +695,28 @@ func (e *enc) encodeJcc(cc int, ops []Operand) error {
// immediate encodes an immediate of the given operand size. full64 selects the
// 64-bit immediate form (only valid for MOV r64, imm64); otherwise a 32-bit
// sign-extended immediate is used for 64-bit operands.
func immediate(v int64, size int, full64 bool) []byte {
//
// The span mirrors go tool asm: every scalar immediate must fit a signed or
// unsigned 32-bit word, and the narrower fields then take the low bits
// silently (ADDB $256, AL encodes imm8 0, MOVW $65536, AX imm16 0). Only the
// imm64 form may exceed the span; anything wider elsewhere is an error rather
// than a truncation the source never asked for.
func immediate(v int64, size int, full64 bool) ([]byte, error) {
if !(size == 8 && full64) && (v < -(1<<31) || v > (1<<32)-1) {
return nil, fmt.Errorf("immediate $%d does not fit in 32 bits", v)
}
switch size {
case 1:
return []byte{byte(int8(v))}
return []byte{byte(int8(v))}, nil
case 2:
return le16(v)
return le16(v), nil
case 4:
return le32(v)
return le32(v), nil
default: // 8
if full64 {
return le64(v)
return le64(v), nil
}
return le32(v) // sign-extended imm32
return le32(v), nil // sign-extended imm32
}
}