diff --git a/asm/assemble.go b/asm/assemble.go index dad212d..f4f360f 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -13,9 +13,10 @@ import ( // Assemble encodes the body of a TEXT function into x86-64 machine code, // resolving local labels to relative jump offsets and translating the FP/SP // pseudo-registers onto the hardware stack pointer (matching the Go -// assembler's default frame-pointer behaviour). Jumps always use the 32-bit -// relative form so instruction sizes are fixed and offsets resolve in a single -// layout pass. +// assembler's default frame-pointer behaviour). Jumps start in the short +// (rel8) form and expand to rel32 when the settled displacement does not fit; +// sizes only grow, so the layout reaches a fixed point in a few passes. CALL +// has no short form and is always rel32. // // Supported operands: registers, memory (real base register), immediates, // FP/SP frame-relative operands, and local-label jumps. SB (global symbol) @@ -23,35 +24,74 @@ import ( // integer and shuffle/extract/permute/move set is in. func Assemble(t *ast.Text) ([]byte, map[string]int, error) { fi := computeFrame(t) + chain := jumpChain(t) + resolve := func(name string) string { + if r, ok := chain[name]; ok { + return r + } + return name + } - // Pass 1: lay out instructions (including prologue/epilogue) to fix label - // offsets. - offsets := map[string]int{} + // Layout: iterate jump sizes to a fixed point. + long := make([]bool, len(t.Body)) sizes := make([]int, len(t.Body)) - pos := len(fi.prologue) - for i, stmt := range t.Body { - switch s := stmt.(type) { - case *ast.Label: - offsets[s.Name.Text] = pos - case *ast.Instr: - sz, err := instrSize(s, fi) - if err != nil { - return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + offsets := map[string]int{} + pcs := make([]int, len(t.Body)) + for { + pos := len(fi.prologue) + for i, stmt := range t.Body { + switch s := stmt.(type) { + case *ast.Label: + offsets[s.Name.Text] = pos + case *ast.Instr: + sz, err := instrSize(s, fi, long[i]) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + } + sizes[i] = sz + pcs[i] = pos + pos += sz } - sizes[i] = sz - pos += sz + } + // Expand any short jump whose displacement no longer fits rel8. + changed := false + for i, stmt := range t.Body { + s, ok := stmt.(*ast.Instr) + if !ok { + continue + } + mnem := strings.ToUpper(s.Mnemonic.Text) + if !isJumpMnemonic(mnem) || mnem == "CALL" || long[i] { + continue + } + name, ok := labelName(s.Operands[0]) + if !ok { + continue // reported during emission + } + target, ok := offsets[resolve(name)] + if !ok { + continue // reported during emission + } + rel := int64(target - (pcs[i] + jumpSize(mnem, false))) + if !fits8(rel) { + long[i] = true + changed = true + } + } + if !changed { + break } } // Pass 2: emit. out := append([]byte(nil), fi.prologue...) - pos = len(fi.prologue) + pos := len(fi.prologue) for i, stmt := range t.Body { s, ok := stmt.(*ast.Instr) if !ok { continue } - code, err := encodeInstr(s, pos, offsets, fi) + code, err := encodeInstr(s, pos, offsets, fi, long[i], resolve) if err != nil { return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) } @@ -64,6 +104,58 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) { return out, offsets, nil } +// jumpChain precomputes jump-to-jump folding: a label whose first instruction +// is an unconditional local jump redirects its own jumpers to the ultimate +// target. The Go toolchain chases exactly these chains (the linker's xfol +// pass) before it encodes branches, so matching its bytes requires the same +// redirection. +func jumpChain(t *ast.Text) map[string]string { + // label → the target of its leading unconditional local JMP, if any. + leadsTo := map[string]string{} + for i, stmt := range t.Body { + l, ok := stmt.(*ast.Label) + if !ok { + continue + } + // Stacked labels share an address: skip to the first instruction. + j := i + 1 + for j < len(t.Body) { + if _, isLabel := t.Body[j].(*ast.Label); !isLabel { + break + } + j++ + } + if j >= len(t.Body) { + continue + } + in, ok := t.Body[j].(*ast.Instr) + if !ok || strings.ToUpper(in.Mnemonic.Text) != "JMP" || len(in.Operands) != 1 { + continue + } + if name, ok := labelName(in.Operands[0]); ok { + leadsTo[l.Name.Text] = name + } + } + // Chase each chain to its end, guarding against cycles. + chain := map[string]string{} + for name := range leadsTo { + visited := map[string]bool{name: true} + cur := name + for { + next, ok := leadsTo[cur] + if !ok || visited[next] { + break + } + visited[next] = true + cur = next + } + if cur != name { + chain[name] = cur + } + } + return chain +} + // frameInfo carries the frame layout derived from the TEXT directive. type frameInfo struct { size int // local frame size ($framesize) @@ -119,15 +211,15 @@ func addSP(size int) []byte { // ADDQ $size, SP return append([]byte{0x48, 0x81, 0xC4}, le32(int64(size))...) } -// instrSize returns the encoded length of an instruction (pass 1). encodeInstr -// already includes the epilogue for a RET in a frame-pointer function; jumps use -// a fixed rel32 size (no epilogue). -func instrSize(s *ast.Instr, fi frameInfo) (int, error) { +// instrSize returns the encoded length of an instruction (layout pass). +// encodeInstr already includes the epilogue for a RET in a frame-pointer +// function; jumps use their short or long form (never an epilogue). +func instrSize(s *ast.Instr, fi frameInfo, long bool) (int, error) { mnem := strings.ToUpper(s.Mnemonic.Text) if isJumpMnemonic(mnem) { - return jumpSize(mnem), nil + return jumpSize(mnem, long), nil } - code, err := encodeInstr(s, 0, nil, fi) + code, err := encodeInstr(s, 0, nil, fi, false, nil) if err != nil { return 0, err } @@ -142,18 +234,27 @@ func isJumpMnemonic(mnem string) bool { return ok } -// jumpSize returns the fixed length of a rel32 jump instruction. -func jumpSize(mnem string) int { - if mnem == "JMP" || mnem == "CALL" { +// jumpSize returns the length of a jump instruction in the requested form: +// short (rel8) where available, otherwise the rel32 form. CALL is always +// rel32. +func jumpSize(mnem string, long bool) int { + if mnem == "CALL" { return 5 // opcode + rel32 } + if !long { + return 2 // opcode + rel8 + } + if mnem == "JMP" { + return 5 // E9 + rel32 + } return 6 // 0x0F 0x8x + rel32 } // encodeInstr encodes one instruction, resolving jump targets against offsets // (relative to pc, the instruction's own offset). A RET in a frame-pointer -// function is prefixed with the epilogue. -func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo) ([]byte, error) { +// function is prefixed with the epilogue. resolve, when non-nil, redirects a +// jump label through the jump-to-jump chain before the offset lookup. +func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string) ([]byte, error) { mnem := strings.ToUpper(s.Mnemonic.Text) var prefix []byte @@ -164,7 +265,7 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo) ([] var code []byte var err error if isJumpMnemonic(mnem) { - code, err = encodeJump(s, mnem, pc+len(prefix), offsets) + code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve) } else { code, err = encodeNormal(s, fi) } @@ -190,9 +291,9 @@ func encodeNormal(s *ast.Instr, fi frameInfo) ([]byte, error) { return Encode(s.Mnemonic.Text, ops...) } -// encodeJump encodes a JMP/CALL/Jcc with a rel32 offset resolved from the -// target label. -func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int) ([]byte, error) { +// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the +// target label, in the short (rel8) or long (rel32) form. +func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long bool, resolve func(string) string) ([]byte, error) { if len(s.Operands) != 1 { return nil, fmt.Errorf("jump expects 1 operand, got %d", len(s.Operands)) } @@ -200,12 +301,25 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int) ([]by if !ok { return nil, fmt.Errorf("jump target must be a local label") } + if resolve != nil && mnem != "CALL" { + name = resolve(name) + } target, ok := offsets[name] if !ok { return nil, fmt.Errorf("undefined label %q", name) } - rel := int64(target - (pc + jumpSize(mnem))) + rel := int64(target - (pc + jumpSize(mnem, long))) + if !long { + if !fits8(rel) { + return nil, fmt.Errorf("jump to %q does not fit the short form", name) + } + if mnem == "JMP" { + return []byte{0xEB, byte(int8(rel))}, nil + } + cc, _ := condCode(mnem) + return []byte{0x70 + byte(cc), byte(int8(rel))}, nil + } switch mnem { case "JMP": return append([]byte{0xE9}, le32(rel)...), nil diff --git a/asm/assemble_test.go b/asm/assemble_test.go index b9598e4..e658cdd 100644 --- a/asm/assemble_test.go +++ b/asm/assemble_test.go @@ -244,3 +244,76 @@ TEXT ·hsum(SB), NOSPLIT, $0 t.Errorf("VEX kernel mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want)) } } + +// TestAssembleShortJumps checks that a tight loop settles on the short (rel8) +// jump forms, byte for byte with the Go assembler. +func TestAssembleShortJumps(t *testing.T) { + fn := firstText(t, ` +#include "textflag.h" +TEXT ·loop(SB), NOSPLIT, $0 + XORQ AX, AX +l1: + ADDQ $1, AX + CMPQ AX, $10 + JLT l1 + RET +`) + code, _, err := Assemble(fn) + if err != nil { + t.Fatalf("Assemble: %v", err) + } + // From the Go-assembled function: + // XORQ AX, AX 4831c0 + // ADDQ $1, AX 4883c001 + // CMPQ AX, $10 4883f80a + // JLT l1 7cf6 (short, rel8) + // RET c3 + want := []byte{ + 0x48, 0x31, 0xc0, + 0x48, 0x83, 0xc0, 0x01, + 0x48, 0x83, 0xf8, 0x0a, + 0x7c, 0xf6, + 0xc3, + } + if hexBytes(code) != hexBytes(want) { + t.Errorf("short-jump mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want)) + } +} + +// TestAssembleJumpFolding checks jump-to-jump folding: a conditional jump to a +// label that only holds an unconditional jump is redirected to the ultimate +// target, exactly as the Go toolchain does before it encodes branches. +func TestAssembleJumpFolding(t *testing.T) { + fn := firstText(t, ` +#include "textflag.h" +TEXT ·fold(SB), NOSPLIT, $0 + XORQ AX, AX + JGE done + INCQ AX +done: + JMP end +end: + RET +`) + code, _, err := Assemble(fn) + if err != nil { + t.Fatalf("Assemble: %v", err) + } + // From the Go-assembled function: the JGE skips past the done: trampoline + // straight to end: + // XORQ AX, AX 4831c0 + // JGE end 7d05 (folded past done) + // INCQ AX 48ffc0 + // JMP end eb00 + // RET c3 + want := []byte{ + 0x48, 0x31, 0xc0, + 0x7d, 0x05, + 0x48, 0xff, 0xc0, + 0xeb, 0x00, + 0xc3, + } + if hexBytes(code) != hexBytes(want) { + t.Errorf("jump-folding mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want)) + } +} diff --git a/asm/encode.go b/asm/encode.go index 412207a..eb85f8e 100644 --- a/asm/encode.go +++ b/asm/encode.go @@ -46,6 +46,14 @@ func (e *enc) encode(mnem string, ops []Operand) error { return e.encodeVex(upper, ops) } + // CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE). + if strings.HasPrefix(upper, "CMOV") { + return e.encodeCmov(upper, ops) + } + if strings.HasPrefix(upper, "SET") { + return e.encodeSet(upper, ops) + } + base, size := splitSize(upper) if size == 0 { size = 8 // default operand size in 64-bit mode (e.g. PUSHQ) @@ -63,12 +71,18 @@ func (e *enc) encode(mnem string, ops []Operand) error { return e.encodeUnary(unaryOp[base], ops, size) case "SHL", "SHR", "SAR": return e.encodeShift(shiftOp[base], ops, size) - case "IMUL": + case "IMUL", "IMUL3": return e.encodeImul(ops, size) case "PUSH": return e.encodePushPop(ops, true) case "POP": return e.encodePushPop(ops, false) + case "LZCNT", "TZCNT": + return e.encodeCount(base, ops, size) + case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX": + return e.encodeMovExtend(base, ops) + case "CVTSL2SD", "CVTSQ2SD": + return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops) } return fmt.Errorf("unsupported instruction %q", mnem) } @@ -100,6 +114,7 @@ type instr struct { rexX bool rexB bool rexForced bool // REX needed even with all bits zero (8-bit low registers) + prefix byte // legacy 0xF2/0xF3 prefix (0 = none); emitted after 0x66 opcode []byte modrm int // -1 if absent sib int // -1 if absent @@ -111,6 +126,9 @@ func (e *enc) emit(i *instr) error { if i.opSize16 { e.out = append(e.out, 0x66) } + if i.prefix != 0 { + e.out = append(e.out, i.prefix) + } rex := byte(0) if i.rexW { rex |= 0x08 diff --git a/asm/encode_test.go b/asm/encode_test.go index 0ff28d9..9d1e6e2 100644 --- a/asm/encode_test.go +++ b/asm/encode_test.go @@ -4,6 +4,7 @@ package asm import ( + "strings" "testing" "golang.org/x/arch/x86/x86asm" @@ -70,7 +71,7 @@ func TestALU(t *testing.T) { checkSyntax(t, "and rbx, 0x7", "ANDQ", Imm(7), BX) checkSyntax(t, "or rcx, rbx", "ORQ", BX, CX) checkSyntax(t, "xor rax, rax", "XORQ", AX, AX) - checkSyntax(t, "cmp r10, rsi", "CMPQ", SI, Reg{idx: 10, size: 8}) + checkSyntax(t, "cmp rsi, r10", "CMPQ", SI, Reg{idx: 10, size: 8}) checkSyntax(t, "add rbx, qword ptr [rax]", "ADDQ", Ptr(AX, 0, 8), BX) checkSyntax(t, "add qword ptr [rax], rbx", "ADDQ", BX, Ptr(AX, 0, 8)) checkSyntax(t, "cmp rbx, -0x20", "CMPQ", Imm(-32), BX) @@ -134,3 +135,116 @@ func TestGoFlacScalarTail(t *testing.T) { checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8}) checkSyntax(t, "and r10, -0x8", "ANDQ", Imm(-8), Reg{idx: 10, size: 8}) } + +// TestScalarGroundTruth checks the scalar instruction families the go-flac +// kernels use beyond the basic set, byte for byte against the Go assembler's +// machine code. wantOp is the x86 decoder's name, which differs from the +// Plan 9 spelling for some of these (CMOVLGT → CMOVG, MOVBLZX → MOVZX, …). +func TestScalarGroundTruth(t *testing.T) { + r8 := Reg{idx: 8, size: 8} + r9 := Reg{idx: 9, size: 8} + r9w := Reg{idx: 9, size: 2} + r8w := Reg{idx: 8, size: 2} + r13 := Reg{idx: 13, size: 8} + cases := []struct { + name string + mnem string + ops []Operand + want string + wantOp string + }{ + {"LZCNTL AX,CX", "LZCNTL", []Operand{AX, CX}, "f30fbdc8", "LZCNT"}, + {"LZCNTQ R8,R9", "LZCNTQ", []Operand{r8, r9}, "f34d0fbdc8", "LZCNT"}, + {"LZCNTW AX,CX", "LZCNTW", []Operand{AX, CX}, "66f30fbdc8", "LZCNT"}, + {"TZCNTL AX,CX", "TZCNTL", []Operand{AX, CX}, "f30fbcc8", "TZCNT"}, + {"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"}, + {"CMOVWLS R9W,R8W", "CMOVWLS", []Operand{r9w, r8w}, "66450f46c1", "CMOVBE"}, + {"SETNE AL", "SETNE", []Operand{AL}, "0f95c0", "SETNE"}, + {"SETNE (AX)", "SETNE", []Operand{Ptr(AX, 0, 1)}, "0f9500", "SETNE"}, + {"MOVBLZX AL,CX", "MOVBLZX", []Operand{AL, CX}, "0fb6c8", "MOVZX"}, + {"MOVBLZX (SI),CX", "MOVBLZX", []Operand{Ptr(SI, 0, 1), CX}, "0fb60e", "MOVZX"}, + {"MOVWLSX (SI)(AX*1),CX", "MOVWLSX", []Operand{Idx(SI, AX, 1, 0, 2), CX}, "0fbf0c06", "MOVSX"}, + {"MOVLQSX CX,R8", "MOVLQSX", []Operand{CX, r8}, "4c63c1", "MOVSXD"}, + {"MOVBQZX AL,R8", "MOVBQZX", []Operand{AL, r8}, "4c0fb6c0", "MOVZX"}, + {"MOVWLZX AX,CX", "MOVWLZX", []Operand{AX, CX}, "0fb7c8", "MOVZX"}, + {"MOVWQZX AX,R8", "MOVWQZX", []Operand{AX, r8}, "4c0fb7c0", "MOVZX"}, + {"CVTSL2SD R8,X13", "CVTSL2SD", []Operand{r8, vreg(t, "X13")}, "f2450f2ae8", "CVTSI2SD"}, + {"CVTSL2SD AX,X0", "CVTSL2SD", []Operand{AX, vreg(t, "X0")}, "f20f2ac0", "CVTSI2SD"}, + {"CVTSQ2SD R8,X13", "CVTSQ2SD", []Operand{r8, vreg(t, "X13")}, "f24d0f2ae8", "CVTSI2SD"}, + {"INCW (R13)(AX*2)", "INCW", []Operand{Idx(r13, AX, 2, 0, 2)}, "6641ff444500", "INC"}, + // The traditional three-operand IMUL spelling. + {"IMUL3L $31,CX,DX", "IMUL3L", []Operand{Imm(31), CX, DX}, "6bd11f", "IMUL"}, + {"IMUL3L $256,CX,DX", "IMUL3L", []Operand{Imm(256), CX, DX}, "69d100010000", "IMUL"}, + {"IMUL3Q $7,R9,R8", "IMUL3Q", []Operand{Imm(7), r9, r8}, "4d6bc107", "IMUL"}, + {"IMUL3W $5,CX,DX", "IMUL3W", []Operand{Imm(5), CX, DX}, "666bd105", "IMUL"}, + // Negative displacement with base + index (regression: the parser + // used to drop the whole address). + {"LEAQ -4(DX)(R9*4),R9", "LEAQ", []Operand{Idx(DX, r9, 4, -4, 8), r9}, "4e8d4c8afc", "LEA"}, + {"LEAQ 16(SI)(BX*4),R10", "LEAQ", []Operand{Idx(SI, BX, 4, 16, 8), Reg{idx: 10, size: 8}}, "4c8d549e10", "LEA"}, + // Register-to-register MOV uses the r/m←r opcode (reg = source), the + // Go assembler's choice. + {"MOVQ BX,R10", "MOVQ", []Operand{BX, Reg{idx: 10, size: 8}}, "4989da", "MOV"}, + {"MOVQ AX,BX", "MOVQ", []Operand{AX, BX}, "4889c3", "MOV"}, + {"MOVL AX,BX", "MOVL", []Operand{AX, BX}, "89c3", "MOV"}, + {"MOVB AL,BL", "MOVB", []Operand{AL, BL}, "88c3", "MOV"}, + {"MOVW AX,BX", "MOVW", []Operand{AX, BX}, "6689c3", "MOV"}, + {"MOVQ R12,R13", "MOVQ", []Operand{Reg{idx: 12, size: 8}, Reg{idx: 13, size: 8}}, "4d89e5", "MOV"}, + // CMP must record first − second: with a register second operand the + // first goes in r/m, with a memory second operand the first goes in reg. + {"CMPQ SI,R10", "CMPQ", []Operand{SI, Reg{idx: 10, size: 8}}, "4c39d6", "CMP"}, + {"CMPQ SI,(AX)", "CMPQ", []Operand{SI, Ptr(AX, 0, 8)}, "483b30", "CMP"}, + {"CMPQ (AX),SI", "CMPQ", []Operand{Ptr(AX, 0, 8), SI}, "483930", "CMP"}, + {"CMPL CX,(AX)", "CMPL", []Operand{CX, Ptr(AX, 0, 4)}, "3b08", "CMP"}, + {"CMPB AL,(BX)", "CMPB", []Operand{AL, Ptr(BX, 0, 1)}, "3a03", "CMP"}, + {"CMPW AX,BX", "CMPW", []Operand{AX, BX}, "6639d8", "CMP"}, + } + for _, c := range cases { + code, err := Encode(c.mnem, c.ops...) + if err != nil { + t.Errorf("%s: Encode: %v", c.name, err) + continue + } + if got := strings.ReplaceAll(hexBytes(code), " ", ""); got != c.want { + t.Errorf("%s: bytes %s, want %s", c.name, got, c.want) + continue + } + inst, err := x86asm.Decode(code, 64) + if err != nil { + t.Errorf("%s: Decode(% x): %v", c.name, code, err) + continue + } + if inst.Op.String() != c.wantOp { + t.Errorf("%s: decoded as %s", c.name, inst.Op.String()) + } + } +} + +// TestScalarErrors checks that malformed conditional / extend / convert +// instructions are rejected. +func TestScalarErrors(t *testing.T) { + cases := []struct { + name string + mnem string + ops []Operand + }{ + {"CMOV arity", "CMOVLGT", []Operand{AX}}, + {"CMOV bare", "CMOV", []Operand{AX, BX}}, + {"CMOV bad size", "CMOVBGT", []Operand{AX, BX}}, + {"CMOV bad condition", "CMOVLXX", []Operand{AX, BX}}, + {"CMOV mem dst", "CMOVLGT", []Operand{AX, Ptr(BX, 0, 4)}}, + {"SET arity", "SETNE", []Operand{AL, BL}}, + {"SET bad condition", "SETXX", []Operand{AL}}, + {"SET bare", "SET", []Operand{AL}}, + {"LZCNT arity", "LZCNTL", []Operand{AX}}, + {"LZCNT mem dst", "LZCNTL", []Operand{AX, Ptr(BX, 0, 4)}}, + {"MOVBLZX mem dst", "MOVBLZX", []Operand{AL, Ptr(BX, 0, 4)}}, + {"CVTSL2SD gpr dst", "CVTSL2SD", []Operand{AX, BX}}, + } + for _, c := range cases { + if _, err := Encode(c.mnem, c.ops...); err == nil { + t.Errorf("%s: expected an error, got none", c.name) + } + } +} diff --git a/asm/instrs.go b/asm/instrs.go index f92ba58..364742b 100644 --- a/asm/instrs.go +++ b/asm/instrs.go @@ -52,9 +52,10 @@ func (e *enc) encodeMov(ops []Operand, size int) error { switch src := src.(type) { case Reg: if dstIsReg { - // MOV r, r/m: 0x8A/0x8B, reg=dst, rm=src. - i := newInstr(size, []byte{movRR(size)}) - if err := setRM(i, dstReg, src, size); err != nil { + // MOV r/m, r: 0x88/0x89, reg=src, rm=dst — the form the Go + // assembler emits for register-to-register moves. + i := newInstr(size, []byte{movRM(size)}) + if err := setRM(i, src, dst, size); err != nil { return err } return e.emit(i) @@ -147,9 +148,37 @@ func (e *enc) encodeALU(op struct { return e.encodeALUImm(op.digit, src, int64(imm), size) } + // CMP records first − second without writing anywhere, so the first + // operand must land as the minuend; every other ALU op writes its second + // operand and follows the forms below. + cmp := op.rr == 0x39 dstReg, dstIsReg := dst.(Reg) srcReg, srcIsReg := src.(Reg) switch { + case cmp && dstIsReg: + // CMP x, reg: OP r/m, r (0x38/0x39) with rm = first operand, reg = + // second, matching the Go assembler. + opc := op.rr + if size == 1 { + opc = op.rr - 1 + } + i := newInstr(size, []byte{opc}) + if err := setRM(i, dstReg, src, size); err != nil { + return err + } + return e.emit(i) + case cmp && srcIsReg: + // CMP reg, mem: OP r, r/m (0x3A/0x3B) with reg = first operand, rm = + // second. + opc := op.rr + 2 + if size == 1 { + opc = op.rr + 1 + } + i := newInstr(size, []byte{opc}) + if err := setRM(i, srcReg, dst, size); err != nil { + return err + } + return e.emit(i) case srcIsReg: // OP r/m, r: reg=src, rm=dst (dst is a register or memory). This is the // form the Go assembler prefers when the source is a register. @@ -497,3 +526,152 @@ func immediate(v int64, size int, full64 bool) []byte { return le32(v) // sign-extended imm32 } } + +// --- CMOVcc / SETcc --------------------------------------------------------- + +// encodeCmov encodes a conditional move: CMOV + size (W/L/Q) + condition +// (CMOVLGT, CMOVQEQ, …). The condition reads exactly like the Jcc spellings; +// the instruction is 0F 40+cc with reg = dst, rm = src. +func (e *enc) encodeCmov(upper string, ops []Operand) error { + if len(ops) != 2 { + return fmt.Errorf("CMOVcc expects 2 operands, got %d", len(ops)) + } + rest := upper[len("CMOV"):] + if len(rest) < 2 { + return fmt.Errorf("unsupported instruction %q", upper) + } + var size int + switch rest[0] { + case 'W': + size = 2 + case 'L': + size = 4 + case 'Q': + size = 8 + default: + return fmt.Errorf("unsupported instruction %q", upper) + } + cc, ok := jccMap[rest[1:]] + if !ok { + return fmt.Errorf("unsupported instruction %q", upper) + } + src, dst := ops[0], ops[1] + dstReg, ok := dst.(Reg) + if !ok { + return fmt.Errorf("CMOVcc destination must be a register") + } + i := newInstr(size, []byte{0x0F, byte(0x40 + cc)}) + if err := setRM(i, dstReg, src, size); err != nil { + return err + } + return e.emit(i) +} + +// encodeSet encodes a conditional byte set: SET + condition (SETNE, SETEQ, …), +// always a byte write — 0F 90+cc /0 into a register or memory operand. +func (e *enc) encodeSet(upper string, ops []Operand) error { + if len(ops) != 1 { + return fmt.Errorf("SETcc expects 1 operand, got %d", len(ops)) + } + cond := upper[len("SET"):] + cc, ok := jccMap[cond] + if !ok || cond == "" { + return fmt.Errorf("unsupported instruction %q", upper) + } + i := &instr{opcode: []byte{0x0F, byte(0x90 + cc)}, modrm: -1, sib: -1} + if err := setRMDigit(i, 0, ops[0], 1); err != nil { + return err + } + return e.emit(i) +} + +// --- LZCNT / TZCNT ---------------------------------------------------------- + +// encodeCount encodes LZCNT/TZCNT (leading / trailing zero count): F3 0F BD +// or F3 0F BC, with reg = dst and rm = src. The size suffix selects the +// operand width (LZCNTW/LZCNTL/LZCNTQ). +func (e *enc) encodeCount(base string, ops []Operand, size int) error { + if len(ops) != 2 { + return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops)) + } + op := byte(0xBD) + if base == "TZCNT" { + op = 0xBC + } + dstReg, ok := ops[1].(Reg) + if !ok { + return fmt.Errorf("%s destination must be a register", base) + } + i := newInstr(size, []byte{0x0F, op}) + i.prefix = 0xF3 + if err := setRM(i, dstReg, ops[0], size); err != nil { + return err + } + return e.emit(i) +} + +// --- mixed-width sign/zero-extending moves ----------------------------------- + +// movExtendOp maps Go's mixed-width move names to their opcode and destination +// width. The source is narrower than the destination, so the plain size-suffix +// convention does not apply to these names. +var movExtendOp = map[string]struct { + op []byte + dst64 bool +}{ + "MOVBLZX": {[]byte{0x0F, 0xB6}, false}, // byte → long, zero-extend + "MOVBQZX": {[]byte{0x0F, 0xB6}, true}, // byte → quad, zero-extend + "MOVWLZX": {[]byte{0x0F, 0xB7}, false}, // word → long, zero-extend + "MOVWQZX": {[]byte{0x0F, 0xB7}, true}, // word → quad, zero-extend + "MOVWLSX": {[]byte{0x0F, 0xBF}, false}, // word → long, sign-extend + "MOVLQSX": {[]byte{0x63}, true}, // long → quad, sign-extend (MOVSXD) +} + +// encodeMovExtend encodes a mixed-width extending move: reg = dst (the wider +// operand), rm = src. +func (e *enc) encodeMovExtend(base string, ops []Operand) error { + if len(ops) != 2 { + return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops)) + } + spec := movExtendOp[base] + dstReg, ok := ops[1].(Reg) + if !ok { + return fmt.Errorf("%s destination must be a register", base) + } + size := 4 + if spec.dst64 { + size = 8 + } + i := newInstr(size, spec.op) + if err := setRM(i, dstReg, ops[0], size); err != nil { + return err + } + return e.emit(i) +} + +// --- CVTSL2SD / CVTSQ2SD ----------------------------------------------------- + +// encodeCvtsi2sd encodes a signed integer to scalar double conversion +// (CVTSL2SD from a 32-bit, CVTSQ2SD from a 64-bit source): F2 0F 2A with +// reg = XMM dst, rm = GPR/memory src. The Go assembler emits the legacy SSE +// encoding here, not the VEX form, so we match it byte for byte. +func (e *enc) encodeCvtsi2sd(quad bool, ops []Operand) error { + if len(ops) != 2 { + return fmt.Errorf("CVTSx2SD expects 2 operands, got %d", len(ops)) + } + src, dst := ops[0], ops[1] + dstReg, ok := dst.(Reg) + if !ok || !dstReg.isVec() { + return fmt.Errorf("CVTSx2SD destination must be a vector register") + } + size := 4 + if quad { + size = 8 + } + i := newInstr(size, []byte{0x0F, 0x2A}) + i.prefix = 0xF2 + if err := setRM(i, dstReg, src, size); err != nil { + return err + } + return e.emit(i) +} diff --git a/asm/vex.go b/asm/vex.go index 94f9569..3934bfa 100644 --- a/asm/vex.go +++ b/asm/vex.go @@ -133,6 +133,17 @@ var vexTable = map[string]vexSpec{ "VZEROUPPER": {1, 0x77, 0, 0, -1, vexZero}, } +// vexVarShift maps the shift mnemonics to their variable-count opcode — the +// form whose count comes from an XMM register or memory (VPSRLQ X0, Y8, Y8), +// an ordinary NDS encoding rather than the /digit immediate form above. +var vexVarShift = map[string]byte{ + "VPSLLD": 0xF2, + "VPSLLQ": 0xF3, + "VPSRAD": 0xE2, + "VPSRLD": 0xD2, + "VPSRLQ": 0xD3, +} + // vexMoveSpec describes a VEX move, which takes different opcodes (and // sometimes a different VEX.W) per operand direction. The Go assembler // encodes a vector→vector move with the store-form opcode (reg = source, @@ -180,6 +191,17 @@ func (e *enc) encodeVex(mnemUpper string, ops []Operand) error { if ms, ok := vexMoveTable[mnemUpper]; ok { return e.encodeVexMove(mnemUpper, ms, ops) } + // The shifts come in two shapes under one mnemonic: an immediate count + // ($imm, src, dst) and a variable count in an XMM register or memory + // (count, src, dst), the latter an ordinary NDS form. + if op, ok := vexVarShift[mnemUpper]; ok && len(ops) == 3 { + if _, isImm := ops[0].(Imm); !isImm { + if !vecOrMem(ops[0]) { + return fmt.Errorf("%s: shift count must be an immediate, a vector register or memory", mnemUpper) + } + return e.encodeVexNDS3(vexSpec{mapSel: 1, opcode: op, pp: 1, opdigit: -1, form: vexNDS3}, ops) + } + } spec := vexTable[mnemUpper] switch spec.form { case vexNDS3: @@ -483,6 +505,15 @@ func vecReg(op Operand) (Reg, bool) { return r, ok && r.isVec() } +// vecOrMem reports whether op is a vector register or a memory reference. +func vecOrMem(op Operand) bool { + if _, ok := op.(Mem); ok { + return true + } + r, ok := op.(Reg) + return ok && r.isVec() +} + // validMoveOther reports whether the non-vector operand of a move is // acceptable: memory always is, a GPR only for VMOVD/VMOVQ. func validMoveOther(ms vexMoveSpec, op Operand) bool { diff --git a/asm/vex_test.go b/asm/vex_test.go index a9bd8b9..742835e 100644 --- a/asm/vex_test.go +++ b/asm/vex_test.go @@ -191,6 +191,14 @@ func TestVexGroundTruth(t *testing.T) { // Immediate shifts. {"VPSLLD $1,Y3,Y4", "VPSLLD", []Operand{Imm(1), vreg(t, "Y3"), vreg(t, "Y4")}, "c5dd72f301"}, {"VPSRLQ $2,Y5,Y6", "VPSRLQ", []Operand{Imm(2), vreg(t, "Y5"), vreg(t, "Y6")}, "c5cd73d502"}, + // Variable-count shifts: the count lives in an XMM register or memory + // and the instruction takes the NDS form. + {"VPSRLQ X0,Y8,Y8", "VPSRLQ", []Operand{vreg(t, "X0"), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd3c0"}, + {"VPSRLQ (AX),Y8,Y8", "VPSRLQ", []Operand{Ptr(AX, 0, 16), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd300"}, + {"VPSLLD X0,Y1,Y2", "VPSLLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f2d0"}, + {"VPSRLD X0,Y1,Y2", "VPSRLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5d2d0"}, + {"VPSRAD X0,Y1,Y2", "VPSRAD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5e2d0"}, + {"VPSLLQ X0,Y1,Y2", "VPSLLQ", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f3d0"}, // Immediate shuffle (reg=dst, rm=src, imm8). {"VPSHUFD $0xEE,X8,X9", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "X8"), vreg(t, "X9")}, "c4417970c8ee"}, {"VPSHUFD $0xEE,Y1,Y2", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "Y1"), vreg(t, "Y2")}, "c5fd70d1ee"}, diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 91c8ded..6c4f5fc 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -26,7 +26,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.2.0" +var version = "0.3.0" func main() { if len(os.Args) < 2 { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e21e6c0..cd44212 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -182,33 +182,39 @@ Every encoding is validated by decoding it again with `golang.org/x/arch` — th one module dependency, used in tests only and never linked into the binary. On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each -operand to an encoder operand, and lays the instructions out in two passes so -local labels resolve to fixed rel32 jump offsets. The `FP`/`SP` pseudo- +operand to an encoder operand, and lays the instructions out so local labels +resolve to relative jump offsets: jumps start in the short (rel8) form and +expand to rel32 when the settled displacement does not fit, iterating to a +fixed point, and jump-to-jump chains are folded (a conditional jump to a label +whose only instruction is an unconditional jump is redirected to the ultimate +target) exactly as the Go toolchain's linker does before it encodes branches. +The `FP`/`SP` pseudo- registers are translated onto the hardware stack pointer — `x+N(FP)` becomes `(N+8)(SP)` for a zero-frame function and `(N+frame+16)(SP)` once a frame pointer is set up, with the matching Go prologue/epilogue generated — so the output is byte-identical to the Go assembler for these cases. SIMD is handled by a VEX (AVX/AVX2) encoder — the two- and three-byte VEX prefixes with XMM/YMM -registers — across seven operand forms: the three-operand NDS form, the -two-operand reg/rm form, the immediate-shift form, the immediate shuffle form -(`VPSHUFD`, `VPERMQ`), the three-operand-plus-immediate form (`VSHUFPD`, +registers — across eight operand forms: the three-operand NDS form, the +two-operand reg/rm form, the immediate-shift form (plus the variable-count +shifts, which share the NDS shape with the count in an XMM register or +memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the +three-operand-plus-immediate form (`VSHUFPD`, `VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`, -lane-extract form (`VEXTRACTI128`, `VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, `VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic (`VADDPD`, `VMULPD`, `VXORPD`, `VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`, -`VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD`, -covering every integer, shuffle and FP instruction the go-flac AVX2 kernels -use. Every encoding is validated two ways: by round-trip decoding -through `golang.org/x/arch`, and byte-for-byte against the machine code the -real Go assembler emits (which also locks the v̄vvv = 1111 rule for unused -vvvv fields — a value the hardware rejects with #UD and the decoder silently -ignores). This increment covers register / memory / immediate / FP-frame -operands, local-label jumps and these VEX SIMD forms; EVEX / AVX-512, `SB` -(global symbol) operands (relocations), a handful of scalar gaps the kernels -hit (`CMOVcc`, `SETcc`, `LZCNT`, `MOVSX`/`MOVZX`) and object-file emission -are the rest of Phase 2. +`VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and +the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves, +`CVTSx2SD`, `IMUL3`), covering every instruction the go-flac AVX2 kernels use +apart from global-symbol loads. Every encoding is validated two ways: by +round-trip decoding through `golang.org/x/arch`, and byte-for-byte against the +machine code the real Go assembler emits — a comparison that now holds for +whole functions: every kernel function that avoids `SB` operands assembles to +exactly the Go toolchain's bytes. This increment covers register / memory / +immediate / FP-frame operands, local-label jumps and these VEX SIMD forms; +EVEX / AVX-512, `SB` (global symbol) operands (relocations) and object-file +emission are the rest of Phase 2. ## Extension points diff --git a/justfile b/justfile index 875ced6..1f1654d 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.2.0" +version := "0.3.0" default: @just --list diff --git a/parser/parser.go b/parser/parser.go index cc3bfb9..16ec4c3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -400,22 +400,29 @@ func parseAddress(g []token.Token) ast.Address { } i := 0 - // Optional leading displacement before a '(' base group. - if isSignedNumber(g, i) && i+1 < len(g) && g[i+1].Kind == token.LParen { - neg := false - if g[i].Kind == token.Minus { - neg = true - i++ - } else if g[i].Kind == token.Plus { - i++ + // Optional leading displacement before a '(' base group. A sign pushes + // the parenthesis one token further out: -4(DX) has it at i+2. + if isSignedNumber(g, i) { + paren := i + 1 + if g[i].Kind == token.Minus || g[i].Kind == token.Plus { + paren = i + 2 } - if i < len(g) && g[i].Kind == token.Number { - addr.Offset = parseInt(g[i].Text) - addr.HasOff = true - if neg { - addr.Offset = -addr.Offset + if paren < len(g) && g[paren].Kind == token.LParen { + neg := false + if g[i].Kind == token.Minus { + neg = true + i++ + } else if g[i].Kind == token.Plus { + i++ + } + if i < len(g) && g[i].Kind == token.Number { + addr.Offset = parseInt(g[i].Text) + addr.HasOff = true + if neg { + addr.Offset = -addr.Offset + } + i++ } - i++ } } // First parenthesised group: the base register. diff --git a/parser/parser_test.go b/parser/parser_test.go index 661a220..28283a0 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -34,6 +34,46 @@ func texts(f *ast.File) []*ast.Text { return out } +// TestNegativeDisplacement is a regression test for a leading negative +// displacement with a base and index: the sign pushed the parenthesis one +// token further out than the lookahead expected, and the whole address used +// to parse empty. +func TestNegativeDisplacement(t *testing.T) { + f, errs := Parse("neg_amd64.s", ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + LEAQ -4(DX)(R9*4), R9 + MOVQ +8(AX), BX + RET +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + fn := texts(f)[0] + var leaq, movq *ast.Instr + for _, s := range fn.Body { + if in, ok := s.(*ast.Instr); ok { + switch in.Mnemonic.Text { + case "LEAQ": + leaq = in + case "MOVQ": + movq = in + } + } + } + if leaq == nil || movq == nil { + t.Fatalf("instructions not parsed: leaq=%v movq=%v", leaq, movq) + } + a := leaq.Operands[0].Addr + if a.Base != "DX" || a.Index != "R9" || a.Scale != 4 || a.Offset != -4 || !a.HasOff { + t.Errorf("LEAQ addr = %+v, want -4(DX)(R9*4)", a) + } + b := movq.Operands[0].Addr + if b.Base != "AX" || b.Offset != 8 || !b.HasOff { + t.Errorf("MOVQ addr = %+v, want +8(AX)", b) + } +} + func TestParseSample(t *testing.T) { f := mustParse(t, "../testdata/sample_amd64.s")