Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a01870695 | ||
|
|
458cfb626e |
+7
-7
@@ -414,19 +414,19 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
|
||||
off := fi.spAdjust + a.Sym.Offset
|
||||
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
|
||||
}
|
||||
// SB (global symbol): a static (file-local, <>) symbol becomes a
|
||||
// RIP-relative reference resolved by the file-level layout; anything
|
||||
// external needs object-file emission.
|
||||
// SB (global symbol): a symbol defined in the same file (GLOBL) is
|
||||
// encoded RIP-relative and resolved by the file-level layout;
|
||||
// anything not defined here needs object-file emission.
|
||||
if a.Sym != nil && a.Sym.Pseudo == "SB" {
|
||||
if !a.Sym.Static {
|
||||
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
||||
}
|
||||
if link == nil || link.symbols == nil {
|
||||
return nil, fmt.Errorf("static symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name)
|
||||
return nil, fmt.Errorf("symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name)
|
||||
}
|
||||
if !link.symbols[a.Sym.Name] {
|
||||
if a.Sym.Static {
|
||||
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
|
||||
}
|
||||
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
||||
}
|
||||
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -49,10 +49,11 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodeJcc(cc, ops)
|
||||
}
|
||||
|
||||
// VEX (AVX/AVX2) instructions: the trailing B/W/L/Q/D is part of the
|
||||
// mnemonic, not a size suffix, so dispatch before splitSize.
|
||||
if isVex(upper) {
|
||||
return e.encodeVex(upper, ops)
|
||||
// VEX (AVX/AVX2) and EVEX (AVX-512) instructions: the trailing
|
||||
// B/W/L/Q/D is part of the mnemonic, not a size suffix, so dispatch
|
||||
// before splitSize.
|
||||
if isVex(upper) || isEvex(upper) || upper == "KMOVW" {
|
||||
return e.encodeVec(upper, ops)
|
||||
}
|
||||
|
||||
// CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE).
|
||||
@@ -92,6 +93,8 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodeMovExtend(base, ops)
|
||||
case "CVTSL2SD", "CVTSQ2SD":
|
||||
return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops)
|
||||
case "MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
return e.encodeSSEMove(sseMoveTable[base], ops)
|
||||
}
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
@@ -114,6 +117,20 @@ func splitSize(upper string) (base string, size int) {
|
||||
return upper, 0
|
||||
}
|
||||
|
||||
// encodeVec dispatches a VEX/EVEX mnemonic to the right encoding: KMOVW has
|
||||
// its own direction-dependent opcodes; KTESTW is always VEX; everything else
|
||||
// takes EVEX when an operand demands it (a ZMM or K register, or an
|
||||
// EVEX-only mnemonic) and VEX otherwise.
|
||||
func (e *enc) encodeVec(upper string, ops []Operand) error {
|
||||
if upper == "KMOVW" {
|
||||
return e.encodeKmovw(ops)
|
||||
}
|
||||
if upper == "KTESTW" || !evexRequired(upper, ops) {
|
||||
return e.encodeVex(upper, ops)
|
||||
}
|
||||
return e.encodeEvex(upper, ops)
|
||||
}
|
||||
|
||||
// --- instruction components -------------------------------------------------
|
||||
|
||||
type instr struct {
|
||||
|
||||
@@ -127,6 +127,52 @@ func TestControl(t *testing.T) {
|
||||
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
||||
}
|
||||
|
||||
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
||||
// against the Go assembler. wantOp is the decoder's name, which differs from
|
||||
// the Plan 9 spelling for the octa moves (MOVOU = MOVDQU, MOVO = MOVDQA).
|
||||
func TestSSEMoveGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
wantOp string
|
||||
}{
|
||||
{"MOVOU (SI),X1", "MOVOU", []Operand{Ptr(SI, 0, 16), vreg(t, "X1")}, "f30f6f0e", "MOVDQU"},
|
||||
{"MOVOU X3,(DI)", "MOVOU", []Operand{vreg(t, "X3"), Ptr(DI, 0, 16)}, "f30f7f1f", "MOVDQU"},
|
||||
{"MOVOU X1,X2", "MOVOU", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "f30f6fd1", "MOVDQU"},
|
||||
{"MOVOU (SI)(BX*4),X9", "MOVOU", []Operand{Idx(SI, BX, 4, 0, 16), vreg(t, "X9")}, "f3440f6f0c9e", "MOVDQU"},
|
||||
{"MOVO (SI),X1", "MOVO", []Operand{Ptr(SI, 0, 16), vreg(t, "X1")}, "660f6f0e", "MOVDQA"},
|
||||
{"MOVO X3,(DI)", "MOVO", []Operand{vreg(t, "X3"), Ptr(DI, 0, 16)}, "660f7f1f", "MOVDQA"},
|
||||
{"MOVUPS (SI),X1", "MOVUPS", []Operand{Ptr(SI, 0, 16), vreg(t, "X1")}, "0f100e", "MOVUPS"},
|
||||
{"MOVAPS X3,(DI)", "MOVAPS", []Operand{vreg(t, "X3"), Ptr(DI, 0, 16)}, "0f291f", "MOVAPS"},
|
||||
{"MOVUPD (SI),X1", "MOVUPD", []Operand{Ptr(SI, 0, 16), vreg(t, "X1")}, "660f100e", "MOVUPD"},
|
||||
{"MOVAPD X3,(DI)", "MOVAPD", []Operand{vreg(t, "X3"), Ptr(DI, 0, 16)}, "660f291f", "MOVAPD"},
|
||||
{"MOVSD (SI),X1", "MOVSD", []Operand{Ptr(SI, 0, 8), vreg(t, "X1")}, "f20f100e", "MOVSD_XMM"},
|
||||
{"MOVSD X1,X2", "MOVSD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "f20f10d1", "MOVSD_XMM"},
|
||||
{"MOVSS X3,(DI)", "MOVSS", []Operand{vreg(t, "X3"), Ptr(DI, 0, 4)}, "f30f111f", "MOVSS"},
|
||||
}
|
||||
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 := hexCompact(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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoFlacScalarTail encodes the scalar tail of an analyze kernel to confirm
|
||||
// the encoder handles a realistic instruction sequence.
|
||||
func TestGoFlacScalarTail(t *testing.T) {
|
||||
|
||||
+558
@@ -0,0 +1,558 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// This file implements EVEX (AVX-512) instruction encoding: the four-byte
|
||||
// EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the
|
||||
// compressed disp8×N displacement, and the operand shapes the go-flac
|
||||
// AVX-512 kernels use. Masking ({k}) and zeroing ({z}) are not supported —
|
||||
// the kernels do not use them. K-register operands (mask destinations,
|
||||
// KMOVW, KTESTW) are.
|
||||
|
||||
// evexSpec describes one EVEX instruction's encoding parameters. The form
|
||||
// field reuses the vexForm shapes, which carry over unchanged.
|
||||
type evexSpec struct {
|
||||
mapSel int // 1 = 0F, 2 = 0F38, 3 = 0F3A
|
||||
opcode byte
|
||||
w int
|
||||
pp int // 0 = none, 1 = 66, 2 = F3, 3 = F2
|
||||
opdigit int // ModRM.reg /digit, or -1 when reg is a register
|
||||
form vexForm // vexNDS3, vexRM, vexShiftImm, vexNDS3Imm, vexExtract
|
||||
n [3]int // disp8×N multiplier per vector length (128/256/512)
|
||||
}
|
||||
|
||||
// evexTable maps an upper-case mnemonic to its EVEX encoding. Mnemonics
|
||||
// that also have a VEX form (VPADDD, VMOVUPD, …) are dispatched here only
|
||||
// when an operand demands EVEX (a ZMM or K register); EVEX-only mnemonics
|
||||
// (VPXORD, VALIGND, …) always encode through this table. The N multipliers
|
||||
// are taken from the Go assembler's opcode tables, which are authoritative
|
||||
// for byte-for-byte agreement.
|
||||
var evexTable = map[string]evexSpec{
|
||||
// EVEX.128/256/512.66.0F — integer arithmetic / logic, NDS form.
|
||||
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPADDQ": {1, 0xD4, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPSUBQ": {1, 0xFB, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPUNPCKLDQ": {1, 0x62, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPUNPCKHDQ": {1, 0x6A, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPXORD": {1, 0xEF, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPXORQ": {1, 0xEF, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPCMPEQD": {1, 0x76, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.128/256/512.66.0F.W1 — packed double arithmetic.
|
||||
"VADDPD": {1, 0x58, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VMULPD": {1, 0x59, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.512.66.0F3A — align (NDS + imm8).
|
||||
"VALIGND": {3, 0x03, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.128/256/512.66.0F — immediate shift (VPSRAD /4).
|
||||
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm, [3]int{16, 32, 64}},
|
||||
// EVEX.128/256/512.66.0F.W1 — variable shift with an XMM count (VPSRAQ;
|
||||
// the W bit distinguishes it from VPSRAD's E2 form).
|
||||
"VPSRAQ": {1, 0xE2, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.128/256/512.F3.0F.W1 — signed qword to packed double (reg=dst,
|
||||
// rm=src, no vvvv).
|
||||
"VCVTQQ2PD": {1, 0xE6, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||
// EVEX.128/256/512.66.0F38.W0 — sign-extend dwords to qwords; the memory
|
||||
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
|
||||
// the xmm/ymm/zmm destination lengths).
|
||||
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||
|
||||
// EVEX.512.66.0F3A.W1 — lane extract (reg=ZMM source, rm=YMM/memory
|
||||
// destination, imm8).
|
||||
"VEXTRACTI64X4": {3, 0x3B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||
"VEXTRACTF64X4": {3, 0x1B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||
|
||||
// EVEX.66.0F38 — more integer NDS forms (W distinguishes D/Q).
|
||||
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
|
||||
|
||||
// EVEX.66.0F — immediate shift (VPSLLD /6).
|
||||
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.F3.0F38.W0 — narrowing stores: reg = wide source, rm = narrow
|
||||
// destination (VPMOVDW dword→word, VPMOVQD qword→dword).
|
||||
"VPMOVDW": {2, 0x33, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
||||
"VPMOVQD": {2, 0x35, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
||||
}
|
||||
|
||||
// evexBcastSpec describes an EVEX broadcast (VPBROADCASTD/Q): the opcode
|
||||
// depends on the source kind — a GPR source uses opReg, a memory source uses
|
||||
// opMem with a disp8×N of n.
|
||||
type evexBcastSpec struct {
|
||||
mapSel int
|
||||
opReg byte
|
||||
opMem byte
|
||||
w int
|
||||
n int
|
||||
}
|
||||
|
||||
var evexBcastTable = map[string]evexBcastSpec{
|
||||
// EVEX.128/256/512.66.0F38 — broadcast a dword/qword to all lanes.
|
||||
"VPBROADCASTD": {2, 0x7C, 0x58, 0, 4},
|
||||
"VPBROADCASTQ": {2, 0x7C, 0x59, 1, 8},
|
||||
}
|
||||
|
||||
// evexMoveSpec describes an EVEX move (load and store opcodes, like the VEX
|
||||
// move table).
|
||||
type evexMoveSpec struct {
|
||||
mapSel int
|
||||
pp int
|
||||
load byte // r/m → vector
|
||||
store byte // vector → r/m
|
||||
w int
|
||||
n [3]int
|
||||
}
|
||||
|
||||
// evexMoveTable maps an upper-case EVEX move mnemonic to its encoding.
|
||||
var evexMoveTable = map[string]evexMoveSpec{
|
||||
// EVEX.128/256/512.F3.0F.W0 — unaligned integer move.
|
||||
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||
// EVEX.128/256/512.F3.0F.W1 — unaligned qword move.
|
||||
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||
// EVEX.128/256/512.66.0F.W1 — unaligned packed double move.
|
||||
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
|
||||
}
|
||||
|
||||
// isEvex reports whether the mnemonic has an EVEX encoding we handle.
|
||||
func isEvex(mnemUpper string) bool {
|
||||
if _, ok := evexTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := evexBcastTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := evexMoveTable[mnemUpper]
|
||||
return ok
|
||||
}
|
||||
|
||||
// evexRequired reports whether the operands force the EVEX encoding of a
|
||||
// mnemonic that also has a VEX form: ZMM and K registers do, and so do
|
||||
// register indices 16–31, which only EVEX can represent (X16–Y31 exist
|
||||
// solely under AVX-512).
|
||||
func evexRequired(upper string, ops []Operand) bool {
|
||||
_, inVex := vexTable[upper]
|
||||
_, inVexMove := vexMoveTable[upper]
|
||||
if !inVex && !inVexMove {
|
||||
return true // EVEX-only mnemonic
|
||||
}
|
||||
for _, op := range ops {
|
||||
if r, ok := op.(Reg); ok && (r.size == 64 || r.mask || (r.isVec() && r.idx >= 16)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order.
|
||||
func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
|
||||
if bs, ok := evexBcastTable[mnemUpper]; ok {
|
||||
return e.encodeEvexBcast(bs, ops)
|
||||
}
|
||||
if ms, ok := evexMoveTable[mnemUpper]; ok {
|
||||
return e.encodeEvexMove(mnemUpper, ms, ops)
|
||||
}
|
||||
spec, ok := evexTable[mnemUpper]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported instruction %q for ZMM/K operands", mnemUpper)
|
||||
}
|
||||
switch spec.form {
|
||||
case vexNDS3:
|
||||
return e.encodeEvexNDS3(spec, ops)
|
||||
case vexRM:
|
||||
return e.encodeEvexRM(spec, ops)
|
||||
case vexRMRev:
|
||||
return e.encodeEvexRMRev(spec, ops)
|
||||
case vexShiftImm:
|
||||
return e.encodeEvexShiftImm(spec, ops)
|
||||
case vexNDS3Imm:
|
||||
return e.encodeEvexNDS3Imm(spec, ops)
|
||||
case vexExtract:
|
||||
return e.encodeEvexExtract(spec, ops)
|
||||
}
|
||||
return fmt.Errorf("unhandled EVEX form for %s", mnemUpper)
|
||||
}
|
||||
|
||||
// encodeEvexNDS3 encodes the three-operand NDS form: OP src2, src1, dst. The
|
||||
// destination may be an opmask register (VPCMPEQD), in which case the vector
|
||||
// length comes from the sources.
|
||||
func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("EVEX NDS instruction expects 3 operands, got %d", len(ops))
|
||||
}
|
||||
src2, src1, dst := ops[0], ops[1], ops[2]
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || (!dstReg.isVec() && !dstReg.mask) {
|
||||
return fmt.Errorf("EVEX destination must be a vector or mask register")
|
||||
}
|
||||
vvvvReg, ok := src1.(Reg)
|
||||
if !ok || !vvvvReg.isVec() {
|
||||
return fmt.Errorf("EVEX vvvv operand must be a vector register")
|
||||
}
|
||||
ll := dstReg.vecLenBit()
|
||||
if dstReg.mask {
|
||||
ll = vvvvReg.vecLenBit()
|
||||
if r, ok := src2.(Reg); ok && r.isVec() {
|
||||
ll = r.vecLenBit()
|
||||
}
|
||||
}
|
||||
return e.emitEvexFields(spec, ll, dstReg.idx, vvvvReg.idx, src2)
|
||||
}
|
||||
|
||||
// encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src,
|
||||
// no vvvv), e.g. VCVTQQ2PD.
|
||||
func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("EVEX destination must be a vector register")
|
||||
}
|
||||
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src)
|
||||
}
|
||||
|
||||
// encodeEvexShiftImm encodes an immediate shift: OP $imm, src, dst
|
||||
// (ModRM.reg = /digit, vvvv = dst, rm = src, imm8), e.g. VPSRAD $31, Z3, Z5.
|
||||
func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("EVEX shift expects 3 operands ($imm, src, dst), got %d", len(ops))
|
||||
}
|
||||
imm, src, dst := ops[0], ops[1], ops[2]
|
||||
immVal, ok := imm.(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("shift count must be an immediate")
|
||||
}
|
||||
srcReg, ok := src.(Reg)
|
||||
if !ok || !srcReg.isVec() {
|
||||
return fmt.Errorf("shift source must be a vector register")
|
||||
}
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("shift destination must be a vector register")
|
||||
}
|
||||
immByte, err := imm8(int64(immVal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), spec.opdigit, dstReg.idx, srcReg); err != nil {
|
||||
return err
|
||||
}
|
||||
e.out = append(e.out, immByte)
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeEvexNDS3Imm encodes OP $imm, src2, src1, dst (reg=dst, vvvv=src1,
|
||||
// rm=src2, imm8), e.g. VALIGND.
|
||||
func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 4 {
|
||||
return fmt.Errorf("instruction expects 4 operands ($imm, src2, src1, dst), got %d", len(ops))
|
||||
}
|
||||
imm, src2, src1, dst := ops[0], ops[1], ops[2], ops[3]
|
||||
immVal, ok := imm.(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("shuffle control must be an immediate")
|
||||
}
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("destination must be a vector register")
|
||||
}
|
||||
vvvvReg, ok := src1.(Reg)
|
||||
if !ok || !vvvvReg.isVec() {
|
||||
return fmt.Errorf("second source must be a vector register")
|
||||
}
|
||||
immByte, err := imm8(int64(immVal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, vvvvReg.idx, src2); err != nil {
|
||||
return err
|
||||
}
|
||||
e.out = append(e.out, immByte)
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeEvexExtract encodes OP $imm, zsrc, ydst (reg=ZMM source, rm=YMM/memory
|
||||
// destination, imm8), e.g. VEXTRACTI64X4.
|
||||
func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("extract expects 3 operands ($imm, zsrc, ydst), got %d", len(ops))
|
||||
}
|
||||
imm, src, dst := ops[0], ops[1], ops[2]
|
||||
immVal, ok := imm.(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("extract lane must be an immediate")
|
||||
}
|
||||
srcReg, ok := src.(Reg)
|
||||
if !ok || !srcReg.isVec() {
|
||||
return fmt.Errorf("extract source must be a vector register")
|
||||
}
|
||||
immByte, err := imm8(int64(immVal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
e.out = append(e.out, immByte)
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeEvexMove encodes a two-operand EVEX move; a vector→vector move uses
|
||||
// the store-form opcode (reg = source, rm = destination), matching the Go
|
||||
// assembler.
|
||||
func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("EVEX move expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
srcReg, srcIsVec := vecReg(src)
|
||||
dstReg, dstIsVec := vecReg(dst)
|
||||
|
||||
op := ms.store
|
||||
var reg Reg
|
||||
var rm Operand
|
||||
switch {
|
||||
case srcIsVec && dstIsVec:
|
||||
reg, rm = srcReg, dst
|
||||
case srcIsVec:
|
||||
if !memOperand(dst) {
|
||||
return fmt.Errorf("%s: invalid destination operand", mnem)
|
||||
}
|
||||
reg, rm = srcReg, dst
|
||||
case dstIsVec:
|
||||
if !memOperand(src) {
|
||||
return fmt.Errorf("%s: invalid source operand", mnem)
|
||||
}
|
||||
op = ms.load
|
||||
reg, rm = dstReg, src
|
||||
default:
|
||||
return fmt.Errorf("%s needs a vector register operand", mnem)
|
||||
}
|
||||
spec := evexSpec{mapSel: ms.mapSel, opcode: op, w: ms.w, pp: ms.pp, opdigit: -1, n: ms.n}
|
||||
return e.emitEvexFields(spec, reg.vecLenBit(), reg.idx, -1, rm)
|
||||
}
|
||||
|
||||
// memOperand reports whether op is a memory reference (including a
|
||||
// static-symbol reference).
|
||||
func memOperand(op Operand) bool {
|
||||
switch op.(type) {
|
||||
case Mem, sbMem:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeEvexRMRev encodes the narrowing-store form: OP src, dst with the wide
|
||||
// source in the reg field and the narrow destination in r/m (VPMOVDW/QD).
|
||||
func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("EVEX store instruction expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
srcReg, ok := src.(Reg)
|
||||
if !ok || !srcReg.isVec() {
|
||||
return fmt.Errorf("EVEX source must be a vector register")
|
||||
}
|
||||
return e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst)
|
||||
}
|
||||
|
||||
// encodeEvexBcast encodes VPBROADCASTD/Q: OP src, dst with the GPR or memory
|
||||
// source broadcast to every lane of the vector destination.
|
||||
func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("broadcast expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("broadcast destination must be a vector register")
|
||||
}
|
||||
spec := evexSpec{mapSel: bs.mapSel, w: bs.w, pp: 1, opdigit: -1}
|
||||
switch src.(type) {
|
||||
case Mem, sbMem:
|
||||
spec.opcode = bs.opMem
|
||||
spec.n = [3]int{bs.n, bs.n, bs.n}
|
||||
case Reg:
|
||||
spec.opcode = bs.opReg
|
||||
default:
|
||||
return fmt.Errorf("broadcast source must be a register or memory")
|
||||
}
|
||||
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src)
|
||||
}
|
||||
|
||||
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
|
||||
// (disp8×N compressed) for the given precomputed fields. regIdx is the
|
||||
// unextended reg-field register index, or a /digit (0–7); vvvvIdx is the
|
||||
// vvvv register index, or -1 when unused.
|
||||
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand) error {
|
||||
if ll > 2 {
|
||||
return fmt.Errorf("invalid vector length")
|
||||
}
|
||||
// reg-field extension bits (R̄, R'̄), inverted.
|
||||
rBar, rPrimeBar := 1, 1
|
||||
if regIdx&8 != 0 {
|
||||
rBar = 0
|
||||
}
|
||||
if regIdx&16 != 0 {
|
||||
rPrimeBar = 0
|
||||
}
|
||||
// vvvv (inverted) and its extension bit V'̄.
|
||||
vBar, vPrimeBar := 15, 1
|
||||
if vvvvIdx >= 0 {
|
||||
vBar = 15 - (vvvvIdx & 15)
|
||||
if vvvvIdx&16 != 0 {
|
||||
vPrimeBar = 0
|
||||
}
|
||||
}
|
||||
|
||||
var modrm, sib int
|
||||
var disp []byte
|
||||
xBar, bBar := 1, 1
|
||||
var sb *sbRef
|
||||
switch r := rm.(type) {
|
||||
case Reg:
|
||||
// ModRM.mod = 11: rm[3] extends via B̄, rm[4] via X̄.
|
||||
modrm = 0xC0 | (regIdx&7)<<3 | (r.idx & 7)
|
||||
sib = -1
|
||||
if r.idx&8 != 0 {
|
||||
bBar = 0
|
||||
}
|
||||
if r.idx&16 != 0 {
|
||||
xBar = 0
|
||||
}
|
||||
case Mem:
|
||||
var err error
|
||||
modrm, sib, disp, xBar, bBar, err = memComponentsEvex(regIdx&7, r, spec.n[ll])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// An indexed memory operand carries index[4] in V'̄ (Go folds it
|
||||
// together with vvvv[4] into the same bit).
|
||||
if r.HasIndex && r.Index.idx&16 != 0 {
|
||||
vPrimeBar = 0
|
||||
}
|
||||
case sbMem:
|
||||
// RIP-relative static-symbol reference; disp32 patched at link time
|
||||
// (no disp8 scaling for RIP-relative addressing).
|
||||
modrm = (regIdx&7)<<3 | 0x05
|
||||
sib = -1
|
||||
disp = le32(0)
|
||||
sb = &sbRef{name: r.name, addend: r.addend}
|
||||
default:
|
||||
return fmt.Errorf("invalid EVEX r/m operand")
|
||||
}
|
||||
|
||||
p0 := byte(rBar<<7 | xBar<<6 | bBar<<5 | rPrimeBar<<4 | spec.mapSel)
|
||||
p1 := byte(spec.w<<7 | vBar<<3 | 1<<2 | spec.pp)
|
||||
p2 := byte(ll<<5 | vPrimeBar<<3) // z = 0, b = 0, aaa = 0
|
||||
e.out = append(e.out, 0x62, p0, p1, p2, spec.opcode, byte(modrm))
|
||||
if sib >= 0 {
|
||||
e.out = append(e.out, byte(sib))
|
||||
}
|
||||
if sb != nil {
|
||||
e.patches = append(e.patches, encPatch{off: len(e.out), name: sb.name, addend: sb.addend})
|
||||
}
|
||||
e.out = append(e.out, disp...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// memComponentsEvex computes the ModR/M byte (with the given reg field), the
|
||||
// SIB byte (-1 if none), the displacement bytes and the (inverted sense)
|
||||
// index/base extension bits for an EVEX memory operand. The displacement is
|
||||
// compressed to disp8×N when it is a multiple of n and the quotient fits a
|
||||
// signed byte; otherwise a full disp32 is used.
|
||||
func memComponentsEvex(regField int, m Mem, n int) (modrm, sib int, disp []byte, xBar, bBar int, err error) {
|
||||
sib = -1
|
||||
xBar, bBar = 1, 1 // inverted bits: 1 = no extension
|
||||
if !m.HasBase && !m.HasIndex {
|
||||
return regField<<3 | 0x05, -1, le32(m.Disp), 1, 1, nil // RIP-relative
|
||||
}
|
||||
|
||||
needSIB := m.HasIndex || (m.HasBase && m.Base.idx&7 == 4)
|
||||
|
||||
var mod int
|
||||
switch {
|
||||
case !m.HasBase:
|
||||
mod = 0
|
||||
disp = le32(m.Disp)
|
||||
case m.Base.idx&7 == 5 && m.Disp == 0:
|
||||
mod = 1
|
||||
disp = []byte{0}
|
||||
case m.Disp == 0:
|
||||
mod = 0
|
||||
case n > 0 && m.Disp%int64(n) == 0 && m.Disp/int64(n) >= -128 && m.Disp/int64(n) <= 127:
|
||||
mod = 1
|
||||
disp = []byte{byte(int8(m.Disp / int64(n)))}
|
||||
default:
|
||||
mod = 2
|
||||
disp = le32(m.Disp)
|
||||
}
|
||||
|
||||
if needSIB {
|
||||
idxField := 4 // 100 = no index
|
||||
if m.HasIndex {
|
||||
idxField = m.Index.idx & 7
|
||||
if m.Index.idx&8 != 0 {
|
||||
xBar = 0
|
||||
}
|
||||
}
|
||||
baseField := 5 // 101 = no base (with mod=00 → disp32)
|
||||
if m.HasBase {
|
||||
baseField = m.Base.idx & 7
|
||||
if m.Base.idx&8 != 0 {
|
||||
bBar = 0
|
||||
}
|
||||
}
|
||||
return mod<<6 | regField<<3 | 0x04, scaleBits(m.Scale)<<6 | idxField<<3 | baseField, disp, xBar, bBar, nil
|
||||
}
|
||||
|
||||
if m.Base.idx&8 != 0 {
|
||||
bBar = 0
|
||||
}
|
||||
return mod<<6 | regField<<3 | (m.Base.idx & 7), -1, disp, 1, bBar, nil
|
||||
}
|
||||
|
||||
// encodeKmovw encodes KMOVW, whose opcode depends on the operand direction:
|
||||
// 90 (k/mem → K), 91 (K → mem), 92 (GPR → K), 93 (K → GPR); k → k uses 90.
|
||||
func (e *enc) encodeKmovw(ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("KMOVW expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
srcReg, srcIsReg := src.(Reg)
|
||||
dstReg, dstIsReg := dst.(Reg)
|
||||
srcK := srcIsReg && srcReg.mask
|
||||
dstK := dstIsReg && dstReg.mask
|
||||
spec := vexSpec{mapSel: 1, w: 0, pp: 0, opdigit: -1}
|
||||
switch {
|
||||
case srcK && dstK:
|
||||
spec.opcode = 0x90 // k ← k: reg = dst, rm = src
|
||||
return e.emitVexFields(spec, 0, dstReg.idx&7, 0, 15, src)
|
||||
case srcK && dstIsReg:
|
||||
spec.opcode = 0x93 // GPR ← k: reg = dst, rm = src
|
||||
rBit := 0
|
||||
if dstReg.idx >= 8 {
|
||||
rBit = 1
|
||||
}
|
||||
return e.emitVexFields(spec, 0, dstReg.idx&7, rBit, 15, src)
|
||||
case srcK:
|
||||
if _, ok := dst.(Mem); !ok {
|
||||
return fmt.Errorf("KMOVW: invalid destination operand")
|
||||
}
|
||||
spec.opcode = 0x91 // mem ← k: reg = src, rm = dst
|
||||
return e.emitVexFields(spec, 0, srcReg.idx&7, 0, 15, dst)
|
||||
case dstK:
|
||||
spec.opcode = 0x92 // k ← GPR/mem: reg = dst, rm = src
|
||||
return e.emitVexFields(spec, 0, dstReg.idx&7, 0, 15, src)
|
||||
}
|
||||
return fmt.Errorf("KMOVW requires a K register operand")
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestEvexGroundTruth checks the EVEX (AVX-512) encodings byte for byte
|
||||
// against machine code extracted from the Go toolchain's assembly of the
|
||||
// same instructions, covering every operand shape the go-flac AVX-512
|
||||
// kernels use: NDS arithmetic, immediate and variable shifts, shuffles with
|
||||
// an immediate, lane extracts, narrowing stores, broadcasts from a GPR or
|
||||
// memory, mask destinations, mask moves, disp8×N compression and the 5-bit
|
||||
// register fields (X/Y 16–31, Z 0–31).
|
||||
func TestEvexGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
// NDS integer arithmetic / logic.
|
||||
{"VPXORD Z12,Z12,Z12", "VPXORD", []Operand{vreg(t, "Z12"), vreg(t, "Z12"), vreg(t, "Z12")}, "62511d48efe4"},
|
||||
{"VPXORQ Z8,Z9,Z10", "VPXORQ", []Operand{vreg(t, "Z8"), vreg(t, "Z9"), vreg(t, "Z10")}, "6251b548efd0"},
|
||||
{"VPADDD Z1,Z0,Z0", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "Z0"), vreg(t, "Z0")}, "62f17d48fec1"},
|
||||
{"VPSUBQ Z8,Z11,Z11", "VPSUBQ", []Operand{vreg(t, "Z8"), vreg(t, "Z11"), vreg(t, "Z11")}, "6251a548fbd8"},
|
||||
{"VPUNPCKLDQ Z5,Z3,Z6", "VPUNPCKLDQ", []Operand{vreg(t, "Z5"), vreg(t, "Z3"), vreg(t, "Z6")}, "62f1654862f5"},
|
||||
{"VPUNPCKHDQ Z5,Z3,Z7", "VPUNPCKHDQ", []Operand{vreg(t, "Z5"), vreg(t, "Z3"), vreg(t, "Z7")}, "62f165486afd"},
|
||||
{"VPMULLQ Z9,Z10,Z10", "VPMULLQ", []Operand{vreg(t, "Z9"), vreg(t, "Z10"), vreg(t, "Z10")}, "6252ad4840d1"},
|
||||
{"VPMULLD Z13,Z11,Z2", "VPMULLD", []Operand{vreg(t, "Z13"), vreg(t, "Z11"), vreg(t, "Z2")}, "62d2254840d5"},
|
||||
{"VPERMD Z0,Z15,Z8", "VPERMD", []Operand{vreg(t, "Z0"), vreg(t, "Z15"), vreg(t, "Z8")}, "6272054836c0"},
|
||||
// Packed-double arithmetic (EVEX forms carry W=1).
|
||||
{"VADDPD Z11,Z10,Z10", "VADDPD", []Operand{vreg(t, "Z11"), vreg(t, "Z10"), vreg(t, "Z10")}, "6251ad4858d3"},
|
||||
{"VMULPD Z13,Z12,Z12", "VMULPD", []Operand{vreg(t, "Z13"), vreg(t, "Z12"), vreg(t, "Z12")}, "62519d4859e5"},
|
||||
{"VFMADD231PD Z14,Z12,Z10", "VFMADD231PD", []Operand{vreg(t, "Z14"), vreg(t, "Z12"), vreg(t, "Z10")}, "62529d48b8d6"},
|
||||
// Align (NDS + imm8).
|
||||
{"VALIGND $12,Z12,Z0,Z1", "VALIGND", []Operand{Imm(12), vreg(t, "Z12"), vreg(t, "Z0"), vreg(t, "Z1")}, "62d37d4803cc0c"},
|
||||
{"VALIGND $15,Z9,Z0,Z1", "VALIGND", []Operand{Imm(15), vreg(t, "Z9"), vreg(t, "Z0"), vreg(t, "Z1")}, "62d37d4803c90f"},
|
||||
// Shifts: immediate (/digit) and variable (XMM count).
|
||||
{"VPSRAD $31,Z3,Z5", "VPSRAD", []Operand{Imm(31), vreg(t, "Z3"), vreg(t, "Z5")}, "62f1554872e31f"},
|
||||
{"VPSLLD $1,Z3,Z4", "VPSLLD", []Operand{Imm(1), vreg(t, "Z3"), vreg(t, "Z4")}, "62f15d4872f301"},
|
||||
{"VPSRAQ X31,Z8,Z8", "VPSRAQ", []Operand{vreg(t, "X31"), vreg(t, "Z8"), vreg(t, "Z8")}, "6211bd48e2c7"},
|
||||
// Mask destinations (the K register occupies the reg field).
|
||||
{"VPCMPEQD Z0,Z3,K1", "VPCMPEQD", []Operand{vreg(t, "Z0"), vreg(t, "Z3"), vreg(t, "K1")}, "62f1654876c8"},
|
||||
{"VPCMPEQD Y30,Y11,K1", "VPCMPEQD", []Operand{vreg(t, "Y30"), vreg(t, "Y11"), vreg(t, "K1")}, "6291252876ce"},
|
||||
// Mask moves and test (VEX-encoded).
|
||||
{"KMOVW K1,CX", "KMOVW", []Operand{vreg(t, "K1"), CX}, "c5f893c9"},
|
||||
{"KMOVW K1,R12", "KMOVW", []Operand{vreg(t, "K1"), vreg(t, "R12")}, "c57893e1"},
|
||||
{"KTESTW K1,K1", "KTESTW", []Operand{vreg(t, "K1"), vreg(t, "K1")}, "c5f899c9"},
|
||||
// Moves, incl. disp8×N (64 for a 512-bit operand).
|
||||
{"VMOVDQU32 (SI)(R15*4),Z3", "VMOVDQU32", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b17e486f1cbe"},
|
||||
{"VMOVDQU32 4(SI)(AX*1),Z4", "VMOVDQU32", []Operand{Idx(SI, AX, 1, 4, 64), vreg(t, "Z4")}, "62f17e486fa40604000000"},
|
||||
{"VMOVDQU32 16(SI)(R15*4),Z4", "VMOVDQU32", []Operand{Idx(SI, vreg(t, "R15"), 4, 16, 64), vreg(t, "Z4")}, "62b17e486fa4be10000000"},
|
||||
{"VMOVDQU32 Z0,4(SI)(AX*1)", "VMOVDQU32", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f17e487f840604000000"},
|
||||
{"VMOVDQU32 Z3,(DI)(R15*4)", "VMOVDQU32", []Operand{vreg(t, "Z3"), Idx(DI, vreg(t, "R15"), 4, 0, 64)}, "62b17e487f1cbf"},
|
||||
// VMOVDQU64 — the W1 qword variant.
|
||||
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
|
||||
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
|
||||
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
|
||||
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
|
||||
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
|
||||
// Conversions and narrowing stores (reg = wide source).
|
||||
{"VCVTQQ2PD Z12,Z12", "VCVTQQ2PD", []Operand{vreg(t, "Z12"), vreg(t, "Z12")}, "6251fe48e6e4"},
|
||||
{"VCVTQQ2PD X13,X13", "VCVTQQ2PD", []Operand{vreg(t, "X13"), vreg(t, "X13")}, "6251fe08e6ed"},
|
||||
{"VPMOVSXDQ 32(SI),Z12", "VPMOVSXDQ", []Operand{Ptr(SI, 32, 32), vreg(t, "Z12")}, "62727d48256601"},
|
||||
{"VPMOVDW Z0,Y0", "VPMOVDW", []Operand{vreg(t, "Z0"), vreg(t, "Y0")}, "62f27e4833c0"},
|
||||
{"VPMOVQD Z11,Y11", "VPMOVQD", []Operand{vreg(t, "Z11"), vreg(t, "Y11")}, "62527e4835db"},
|
||||
// Lane extracts.
|
||||
{"VEXTRACTI64X4 $1,Z8,Y9", "VEXTRACTI64X4", []Operand{Imm(1), vreg(t, "Z8"), vreg(t, "Y9")}, "6253fd483bc101"},
|
||||
{"VEXTRACTF64X4 $1,Z10,Y11", "VEXTRACTF64X4", []Operand{Imm(1), vreg(t, "Z10"), vreg(t, "Y11")}, "6253fd481bd301"},
|
||||
// Broadcasts: GPR source (0x7C) vs memory source (0x58/0x59, disp8×4/8).
|
||||
{"VPBROADCASTD AX,Z15", "VPBROADCASTD", []Operand{AX, vreg(t, "Z15")}, "62727d487cf8"},
|
||||
{"VPBROADCASTD (SI),Z8", "VPBROADCASTD", []Operand{Ptr(SI, 0, 4), vreg(t, "Z8")}, "62727d485806"},
|
||||
{"VPBROADCASTD 4(SI),Z10", "VPBROADCASTD", []Operand{Ptr(SI, 4, 4), vreg(t, "Z10")}, "62727d48585601"},
|
||||
{"VPBROADCASTQ R8,X31", "VPBROADCASTQ", []Operand{vreg(t, "R8"), vreg(t, "X31")}, "6242fd087cf8"},
|
||||
{"VPBROADCASTQ AX,Z9", "VPBROADCASTQ", []Operand{AX, vreg(t, "Z9")}, "6272fd487cc8"},
|
||||
// Register indices 16–31 exist only in EVEX encodings.
|
||||
{"VPBROADCASTD AX,Y30", "VPBROADCASTD", []Operand{AX, vreg(t, "Y30")}, "62627d287cf0"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
want := strings.ReplaceAll(c.want, " ", "")
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Encode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := hexCompact(code); got != want {
|
||||
t.Errorf("%s: bytes %s, want %s", c.name, got, want)
|
||||
continue
|
||||
}
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
|
||||
continue
|
||||
}
|
||||
if inst.Len != len(code) {
|
||||
t.Errorf("%s: Decode consumed %d of %d bytes", c.name, inst.Len, len(code))
|
||||
}
|
||||
if inst.Op.String() != c.mnem {
|
||||
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexErrors checks the EVEX-specific error paths.
|
||||
func TestEvexErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
}{
|
||||
{"NDS arity", "VPXORD", []Operand{vreg(t, "Z0"), vreg(t, "Z1")}},
|
||||
{"KMOVW arity", "KMOVW", []Operand{vreg(t, "K1")}},
|
||||
{"KMOVW no K", "KMOVW", []Operand{AX, CX}},
|
||||
{"VMOVUPD Z gpr", "VMOVUPD", []Operand{AX, vreg(t, "Z1")}},
|
||||
{"broadcast src", "VPBROADCASTD", []Operand{Imm(1), vreg(t, "Z1")}},
|
||||
{"VPMOVDW src", "VPMOVDW", []Operand{AX, vreg(t, "Y0")}},
|
||||
{"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}},
|
||||
// VEX-only mnemonics reject registers only EVEX can encode.
|
||||
{"VPSHUFD X16", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
||||
t.Errorf("%s: expected an error, got none", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
||||
// that the static-symbol load resolves to the right bytes in the image.
|
||||
// Skipped when the sibling repository is not checked out.
|
||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 10 {
|
||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||
}
|
||||
|
||||
// idx16 as the DATA directives define it: dwords 1..16.
|
||||
idx := make([]byte, 0, 64)
|
||||
for i := 1; i <= 16; i++ {
|
||||
idx = append(idx, byte(i), 0, 0, 0)
|
||||
}
|
||||
image := img.Bytes()
|
||||
base := img.Symbols["idx16"]
|
||||
if base == 0 {
|
||||
t.Fatal("idx16 not laid out")
|
||||
}
|
||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||
}
|
||||
|
||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||
// resolve to idx16 within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||
for pos := 0; ; {
|
||||
i := indexOf(code[pos:], pat)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
i += pos
|
||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||
target := fn.Offset + i + 10 + int(rel)
|
||||
if target != base {
|
||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||
}
|
||||
loads++
|
||||
pos = i + 10
|
||||
}
|
||||
}
|
||||
if loads != 1 {
|
||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// hexCompact renders bytes as a lowercase hex string without separators.
|
||||
func hexCompact(b []byte) string {
|
||||
const hexdig = "0123456789abcdef"
|
||||
out := make([]byte, len(b)*2)
|
||||
for i, c := range b {
|
||||
out[i*2] = hexdig[c>>4]
|
||||
out[i*2+1] = hexdig[c&0xf]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||
func indexOf(b, pat []byte) int {
|
||||
for i := 0; i+len(pat) <= len(b); i++ {
|
||||
j := 0
|
||||
for j < len(pat) && b[i+j] == pat[j] {
|
||||
j++
|
||||
}
|
||||
if j == len(pat) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -661,6 +661,66 @@ func (e *enc) encodeMovExtend(base string, ops []Operand) error {
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- legacy SSE moves --------------------------------------------------------
|
||||
|
||||
// sseMove describes a legacy (non-VEX) SSE move: a mandatory prefix plus a
|
||||
// load opcode (reg = destination, rm = source) and a store opcode (the
|
||||
// reverse). The Plan 9 names MOVOU/MOVO are the integer unaligned/aligned
|
||||
// octa moves (MOVDQU/MOVDQA), not the packed-single ones.
|
||||
type sseMove struct {
|
||||
prefix byte // 0, 0x66, 0xF2 or 0xF3
|
||||
load byte
|
||||
store byte
|
||||
}
|
||||
|
||||
var sseMoveTable = map[string]sseMove{
|
||||
"MOVOU": {0xF3, 0x6F, 0x7F}, // MOVDQU — unaligned octa
|
||||
"MOVO": {0x66, 0x6F, 0x7F}, // MOVDQA — aligned octa
|
||||
"MOVUPS": {0x00, 0x10, 0x11}, // unaligned packed single
|
||||
"MOVAPS": {0x00, 0x28, 0x29}, // aligned packed single
|
||||
"MOVUPD": {0x66, 0x10, 0x11}, // unaligned packed double
|
||||
"MOVAPD": {0x66, 0x28, 0x29}, // aligned packed double
|
||||
"MOVSD": {0xF2, 0x10, 0x11}, // scalar double
|
||||
"MOVSS": {0xF3, 0x10, 0x11}, // scalar single
|
||||
}
|
||||
|
||||
// encodeSSEMove encodes a legacy SSE move: a vector-to-vector move uses the
|
||||
// load form (reg = destination), matching the Go assembler.
|
||||
func (e *enc) encodeSSEMove(m sseMove, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("SSE move expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
srcReg, srcVec := vecReg(src)
|
||||
dstReg, dstVec := vecReg(dst)
|
||||
op := m.store
|
||||
var reg Reg
|
||||
var rm Operand
|
||||
switch {
|
||||
case srcVec && dstVec:
|
||||
op = m.load
|
||||
reg, rm = dstReg, src
|
||||
case srcVec:
|
||||
if _, ok := dst.(Mem); !ok {
|
||||
return fmt.Errorf("SSE move: invalid destination operand")
|
||||
}
|
||||
reg, rm = srcReg, dst
|
||||
case dstVec:
|
||||
if _, ok := src.(Mem); !ok {
|
||||
return fmt.Errorf("SSE move: invalid source operand")
|
||||
}
|
||||
op = m.load
|
||||
reg, rm = dstReg, src
|
||||
default:
|
||||
return fmt.Errorf("SSE move needs a vector register operand")
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, reg, rm, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- CVTSL2SD / CVTSQ2SD -----------------------------------------------------
|
||||
|
||||
// encodeCvtsi2sd encodes a signed integer to scalar double conversion
|
||||
|
||||
+69
-55
@@ -14,19 +14,24 @@ import "strings"
|
||||
// so the encoder keys off the register's index and lets the mnemonic supply the
|
||||
// size. The high flag marks the legacy high-byte registers AH/CH/DH/BH, which
|
||||
// occupy indices 4–7 yet take no REX prefix, unlike SPL/BPL/SIL/DIL that share
|
||||
// those indices but require one.
|
||||
// those indices but require one. The mask flag marks the AVX-512 opmask
|
||||
// registers K0–K7.
|
||||
type Reg struct {
|
||||
idx int
|
||||
size int // informational width implied by the name; the mnemonic decides
|
||||
high bool // AH/CH/DH/BH
|
||||
mask bool // K0–K7 opmask register
|
||||
}
|
||||
|
||||
// Index returns the register number (0–15).
|
||||
// Index returns the register number (0–15 for GPRs, 0–31 for vectors).
|
||||
func (r Reg) Index() int { return r.idx }
|
||||
|
||||
// Size returns the width in bytes implied by the register's name.
|
||||
func (r Reg) Size() int { return r.size }
|
||||
|
||||
// IsMask reports whether r is an AVX-512 opmask register (K0–K7).
|
||||
func (r Reg) IsMask() bool { return r.mask }
|
||||
|
||||
func (r Reg) isOperand() {}
|
||||
|
||||
// needsREX reports whether this register forces a REX prefix at the given
|
||||
@@ -41,45 +46,45 @@ func (r Reg) needsREX(opSize int) bool {
|
||||
|
||||
// Register constants (the size is the width the name implies).
|
||||
var (
|
||||
AL = Reg{0, 1, false}
|
||||
CL = Reg{1, 1, false}
|
||||
DL = Reg{2, 1, false}
|
||||
BL = Reg{3, 1, false}
|
||||
AH = Reg{4, 1, true}
|
||||
CH = Reg{5, 1, true}
|
||||
DH = Reg{6, 1, true}
|
||||
BH = Reg{7, 1, true}
|
||||
SPL = Reg{4, 1, false}
|
||||
BPL = Reg{5, 1, false}
|
||||
SIL = Reg{6, 1, false}
|
||||
DIL = Reg{7, 1, false}
|
||||
AL = Reg{idx: 0, size: 1}
|
||||
CL = Reg{idx: 1, size: 1}
|
||||
DL = Reg{idx: 2, size: 1}
|
||||
BL = Reg{idx: 3, size: 1}
|
||||
AH = Reg{idx: 4, size: 1, high: true}
|
||||
CH = Reg{idx: 5, size: 1, high: true}
|
||||
DH = Reg{idx: 6, size: 1, high: true}
|
||||
BH = Reg{idx: 7, size: 1, high: true}
|
||||
SPL = Reg{idx: 4, size: 1}
|
||||
BPL = Reg{idx: 5, size: 1}
|
||||
SIL = Reg{idx: 6, size: 1}
|
||||
DIL = Reg{idx: 7, size: 1}
|
||||
|
||||
AX = Reg{0, 2, false}
|
||||
CX = Reg{1, 2, false}
|
||||
DX = Reg{2, 2, false}
|
||||
BX = Reg{3, 2, false}
|
||||
SP = Reg{4, 2, false}
|
||||
BP = Reg{5, 2, false}
|
||||
SI = Reg{6, 2, false}
|
||||
DI = Reg{7, 2, false}
|
||||
AX = Reg{idx: 0, size: 2}
|
||||
CX = Reg{idx: 1, size: 2}
|
||||
DX = Reg{idx: 2, size: 2}
|
||||
BX = Reg{idx: 3, size: 2}
|
||||
SP = Reg{idx: 4, size: 2}
|
||||
BP = Reg{idx: 5, size: 2}
|
||||
SI = Reg{idx: 6, size: 2}
|
||||
DI = Reg{idx: 7, size: 2}
|
||||
|
||||
EAX = Reg{0, 4, false}
|
||||
ECX = Reg{1, 4, false}
|
||||
EDX = Reg{2, 4, false}
|
||||
EBX = Reg{3, 4, false}
|
||||
ESP = Reg{4, 4, false}
|
||||
EBP = Reg{5, 4, false}
|
||||
ESI = Reg{6, 4, false}
|
||||
EDI = Reg{7, 4, false}
|
||||
EAX = Reg{idx: 0, size: 4}
|
||||
ECX = Reg{idx: 1, size: 4}
|
||||
EDX = Reg{idx: 2, size: 4}
|
||||
EBX = Reg{idx: 3, size: 4}
|
||||
ESP = Reg{idx: 4, size: 4}
|
||||
EBP = Reg{idx: 5, size: 4}
|
||||
ESI = Reg{idx: 6, size: 4}
|
||||
EDI = Reg{idx: 7, size: 4}
|
||||
|
||||
RAX = Reg{0, 8, false}
|
||||
RCX = Reg{1, 8, false}
|
||||
RDX = Reg{2, 8, false}
|
||||
RBX = Reg{3, 8, false}
|
||||
RSP = Reg{4, 8, false}
|
||||
RBP = Reg{5, 8, false}
|
||||
RSI = Reg{6, 8, false}
|
||||
RDI = Reg{7, 8, false}
|
||||
RAX = Reg{idx: 0, size: 8}
|
||||
RCX = Reg{idx: 1, size: 8}
|
||||
RDX = Reg{idx: 2, size: 8}
|
||||
RBX = Reg{idx: 3, size: 8}
|
||||
RSP = Reg{idx: 4, size: 8}
|
||||
RBP = Reg{idx: 5, size: 8}
|
||||
RSI = Reg{idx: 6, size: 8}
|
||||
RDI = Reg{idx: 7, size: 8}
|
||||
)
|
||||
|
||||
// regByName maps an assembly register name (case-insensitive) to a Reg.
|
||||
@@ -91,28 +96,28 @@ func buildRegByName() map[string]Reg {
|
||||
// 64-bit: RAX..RDI, R8..R15.
|
||||
r64 := []string{"RAX", "RCX", "RDX", "RBX", "RSP", "RBP", "RSI", "RDI"}
|
||||
for i, n := range r64 {
|
||||
m[n] = Reg{i, 8, false}
|
||||
m[n] = Reg{idx: i, size: 8}
|
||||
}
|
||||
for i := 8; i <= 15; i++ {
|
||||
m["R"+itoa(i)] = Reg{i, 8, false}
|
||||
m["R"+itoa(i)] = Reg{idx: i, size: 8}
|
||||
}
|
||||
|
||||
// 32-bit: EAX..EDI, R8D..R15D.
|
||||
e32 := []string{"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI"}
|
||||
for i, n := range e32 {
|
||||
m[n] = Reg{i, 4, false}
|
||||
m[n] = Reg{idx: i, size: 4}
|
||||
}
|
||||
for i := 8; i <= 15; i++ {
|
||||
m["R"+itoa(i)+"D"] = Reg{i, 4, false}
|
||||
m["R"+itoa(i)+"D"] = Reg{idx: i, size: 4}
|
||||
}
|
||||
|
||||
// 16-bit: AX..DI, R8W..R15W.
|
||||
w16 := []string{"AX", "CX", "DX", "BX", "SP", "BP", "SI", "DI"}
|
||||
for i, n := range w16 {
|
||||
m[n] = Reg{i, 2, false}
|
||||
m[n] = Reg{idx: i, size: 2}
|
||||
}
|
||||
for i := 8; i <= 15; i++ {
|
||||
m["R"+itoa(i)+"W"] = Reg{i, 2, false}
|
||||
m["R"+itoa(i)+"W"] = Reg{idx: i, size: 2}
|
||||
}
|
||||
|
||||
// 8-bit: AL..BH, SPL..DIL, R8B..R15B.
|
||||
@@ -124,25 +129,34 @@ func buildRegByName() map[string]Reg {
|
||||
m[n] = r
|
||||
}
|
||||
for i := 8; i <= 15; i++ {
|
||||
m["R"+itoa(i)+"B"] = Reg{i, 1, false}
|
||||
m["R"+itoa(i)+"B"] = Reg{idx: i, size: 1}
|
||||
}
|
||||
|
||||
// Vector: X0..X15 (128-bit, encoded size 16), Y0..Y15 (256-bit, size 32).
|
||||
// Z (512-bit) and K (mask) registers arrive with EVEX/AVX-512 support.
|
||||
for i := 0; i <= 15; i++ {
|
||||
m["X"+itoa(i)] = Reg{i, 16, false}
|
||||
m["Y"+itoa(i)] = Reg{i, 32, false}
|
||||
// Vector: X0..X31 (128-bit, size 16), Y0..Y31 (256-bit, size 32),
|
||||
// Z0..Z31 (512-bit, size 64). Indices 16–31 are only encodable in EVEX
|
||||
// (AVX-512) instructions; the encoder validates that through its tables.
|
||||
for i := 0; i <= 31; i++ {
|
||||
m["X"+itoa(i)] = Reg{idx: i, size: 16}
|
||||
m["Y"+itoa(i)] = Reg{idx: i, size: 32}
|
||||
m["Z"+itoa(i)] = Reg{idx: i, size: 64}
|
||||
}
|
||||
// Opmask: K0..K7.
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["K"+itoa(i)] = Reg{idx: i, size: 8, mask: true}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// isVec reports whether r is an XMM/YMM vector register.
|
||||
func (r Reg) isVec() bool { return r.size == 16 || r.size == 32 }
|
||||
// isVec reports whether r is an XMM/YMM/ZMM vector register.
|
||||
func (r Reg) isVec() bool { return r.size == 16 || r.size == 32 || r.size == 64 }
|
||||
|
||||
// vecLenBit returns the VEX.L bit for a vector register (X=0/128-bit,
|
||||
// Y=1/256-bit).
|
||||
// vecLenBit returns the vector-length field for a vector register:
|
||||
// 0 (128-bit, VEX.L / EVEX.L'L=00), 1 (256-bit) or 2 (512-bit, EVEX only).
|
||||
func (r Reg) vecLenBit() int {
|
||||
if r.size == 32 {
|
||||
switch r.size {
|
||||
case 64:
|
||||
return 2
|
||||
case 32:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
|
||||
+17
@@ -39,6 +39,10 @@ const (
|
||||
// source lives in the reg field, the destination in r/m — the PEXTR-style
|
||||
// layout. VEXTRACTI128 and VEXTRACTF128 use this shape.
|
||||
vexExtract
|
||||
// vexRMRev is the reversed two-operand form `OP src, dst` with the source
|
||||
// in ModRM.reg and the destination in r/m — the layout of the EVEX
|
||||
// narrowing stores (VPMOVDW, VPMOVQD).
|
||||
vexRMRev
|
||||
// vexZero is the no-operand form (VZEROUPPER).
|
||||
vexZero
|
||||
)
|
||||
@@ -131,6 +135,9 @@ var vexTable = map[string]vexSpec{
|
||||
|
||||
// VEX.128.0F.W0 — no operands.
|
||||
"VZEROUPPER": {1, 0x77, 0, 0, -1, vexZero},
|
||||
|
||||
// VEX.128.0F.W0 — mask-register test (KTESTW k1, k2: reg = dst, rm = src).
|
||||
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
|
||||
}
|
||||
|
||||
// vexVarShift maps the shift mnemonics to their variable-count opcode — the
|
||||
@@ -188,6 +195,13 @@ func isVex(mnemUpper string) bool {
|
||||
|
||||
// encodeVex encodes a VEX instruction with operands in Plan 9 order.
|
||||
func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
||||
// Vector register indices 16–31 exist only in EVEX encodings; fail
|
||||
// loudly rather than silently truncating the index.
|
||||
for _, op := range ops {
|
||||
if r, ok := op.(Reg); ok && r.isVec() && r.idx >= 16 {
|
||||
return fmt.Errorf("%s: vector register index %d needs an EVEX (AVX-512) instruction", mnemUpper, r.idx)
|
||||
}
|
||||
}
|
||||
if ms, ok := vexMoveTable[mnemUpper]; ok {
|
||||
return e.encodeVexMove(mnemUpper, ms, ops)
|
||||
}
|
||||
@@ -531,6 +545,9 @@ func validMoveOther(ms vexMoveSpec, op Operand) bool {
|
||||
// the given precomputed fields. It is shared by every register/rm VEX form;
|
||||
// immediate bytes are appended by the caller.
|
||||
func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Operand) error {
|
||||
if l > 1 {
|
||||
return fmt.Errorf("ZMM operand requires an EVEX instruction")
|
||||
}
|
||||
var modrm, sib int
|
||||
var disp []byte
|
||||
var xBit, bBit int
|
||||
|
||||
+1
-1
@@ -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.4.0"
|
||||
var version = "0.6.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
|
||||
+23
-15
@@ -136,13 +136,18 @@ Two deeper analyses sit on top of the AST:
|
||||
control-flow graph (basic blocks split at labels and after branches, with
|
||||
fall-through and jump-target edges), computes a conservative per-instruction
|
||||
register def/use, and runs the standard backward liveness iteration to a fixed
|
||||
point. On top of that it flags a **callee-saved register that is written but
|
||||
never saved and restored** — the per-architecture callee-saved set is amd64
|
||||
`BX/BP/R12–R15`, arm64 `R19–R30`, riscv64 `X1/X8/X9/X18–X27`, loong64
|
||||
`R1/R22–R31`. This is an *audit*: the runtime's own assembly clobbers these
|
||||
registers freely (it controls both sides of the call), so the rule is
|
||||
advisory there, but in hand-written kernels called from ordinary Go code a
|
||||
clobber is a genuine ABI violation. It runs only on macro-free files, where
|
||||
point. On top of that it flags writes to the registers the **Go ABI** fixes
|
||||
across calls that are never saved and restored — calibrated from
|
||||
`cmd/compile/abi-internal.md`, *not* the platform ABI: Go's stack-based ABI0
|
||||
has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and
|
||||
the like are caller-saved or permanent scratch, and hand-written kernels may
|
||||
clobber them freely). The audited set is the frame pointer and the
|
||||
goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
|
||||
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
|
||||
when the function can reach the runtime — it is not `NOSPLIT` or makes a
|
||||
call — since the ABI0 transition machinery restores it on those paths, and
|
||||
NOSPLIT call-free leaves may use it (the runtime's own assembly does). It
|
||||
runs only on macro-free files, where
|
||||
no opaque macro can perform the save/restore.
|
||||
- **`funcdata-pcdata`.** `FUNCDATA $idx, sym(SB)` and `PCDATA $idx, $val` are
|
||||
checked for well-formed operands (arity, immediate index and value, symbol
|
||||
@@ -206,13 +211,16 @@ memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
|
||||
`VMULPD`, `VXORPD`, `VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`,
|
||||
`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. 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 holds for the whole kernel: all 17
|
||||
functions of the go-flac AVX2 file assemble to exactly the Go toolchain's
|
||||
bytes, the lone exception being the displacements of the static-constant
|
||||
loads, which the Go linker fills at link time.
|
||||
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with
|
||||
5-bit register fields (Z0–Z31, X/Y 16–31), opmask registers as operands and
|
||||
mask destinations, and the compressed disp8×N displacement, whose multiplier
|
||||
follows the memory operand's size — covering every instruction the go-flac
|
||||
AVX2 and AVX-512 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 — a comparison that holds for
|
||||
whole functions: all 27 functions of both kernels assemble to exactly the Go
|
||||
toolchain's bytes, the lone exception being the displacements of the
|
||||
static-constant loads, which the Go linker fills at link time.
|
||||
|
||||
File-level assembly (`AssembleFile`) goes beyond single functions: it
|
||||
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
||||
@@ -220,7 +228,7 @@ behind the code and resolves references to them (`mask<>(SB)`) to
|
||||
RIP-relative loads whose displacements point inside the resulting image, so
|
||||
the bytes are self-consistent at any base address. External (non-file-local)
|
||||
symbols are rejected: they need object-file emission, which — together with
|
||||
EVEX / AVX-512 and the other architectures — is the rest of Phase 2.
|
||||
EVEX masking/zeroing and the other architectures — is the rest of Phase 2.
|
||||
|
||||
## Extension points
|
||||
|
||||
|
||||
@@ -99,6 +99,12 @@ func Source(path, src string) string {
|
||||
}
|
||||
case kInstr:
|
||||
out = renderInstr(line, maxWidth[inf.funcID])
|
||||
// A RET ends the body for indentation purposes: comments that
|
||||
// follow it — typically the next function's doc comment — belong
|
||||
// at column 0, not inside the finished function.
|
||||
if strings.EqualFold(line[0].Text, "RET") {
|
||||
inBody = false
|
||||
}
|
||||
}
|
||||
b.WriteString(strings.TrimRight(out, " \t"))
|
||||
b.WriteByte('\n')
|
||||
|
||||
@@ -39,6 +39,44 @@ func TestGolden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocCommentIndent checks that a doc comment preceding a TEXT directive
|
||||
// sits at column 0 even when another function (ending in RET) precedes it —
|
||||
// the RET must terminate the previous body for indentation purposes.
|
||||
func TestDocCommentIndent(t *testing.T) {
|
||||
in := "#include \"textflag.h\"\n" +
|
||||
"\n" +
|
||||
"// func first()\n" +
|
||||
"TEXT ·first(SB), NOSPLIT, $0\n" +
|
||||
"XORQ AX, AX\n" +
|
||||
"RET\n" +
|
||||
"\n" +
|
||||
"// func second()\n" +
|
||||
"TEXT ·second(SB), NOSPLIT, $0\n" +
|
||||
"RET\n"
|
||||
|
||||
want := "#include \"textflag.h\"\n" +
|
||||
"\n" +
|
||||
"// func first()\n" +
|
||||
"TEXT ·first(SB), NOSPLIT, $0\n" +
|
||||
"\tXORQ AX, AX\n" +
|
||||
"\tRET\n" +
|
||||
"\n" +
|
||||
"// func second()\n" +
|
||||
"TEXT ·second(SB), NOSPLIT, $0\n" +
|
||||
"\tRET\n"
|
||||
|
||||
got := Source("d_amd64.s", in)
|
||||
if got != want {
|
||||
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
||||
}
|
||||
// Body comments stay indented.
|
||||
body := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n// inside the body\nXORQ AX, AX\nRET\n"
|
||||
gotBody := Source("b_amd64.s", body)
|
||||
if !strings.Contains(gotBody, "\t// inside the body\n") {
|
||||
t.Fatalf("body comment must stay indented:\n%q", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperandSpacing(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"4(SI)": "4(SI)",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||
|
||||
version := "0.4.0"
|
||||
version := "0.6.0"
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
+38
-6
@@ -319,18 +319,27 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
}
|
||||
}
|
||||
|
||||
// Register liveness: a callee-saved register that is written but never
|
||||
// saved and restored is clobbered across the call. The check runs over the
|
||||
// control-flow graph and is skipped for macro-using files, where an opaque
|
||||
// macro may perform the save/restore.
|
||||
// Register liveness: a register the Go ABI fixes across calls that is
|
||||
// written but never saved and restored is clobbered. The check runs over
|
||||
// the control-flow graph and is skipped for macro-using files, where an
|
||||
// opaque macro may perform the save/restore.
|
||||
if doLabelChecks && archKnown && !cfg.Disable[CodeRegisterClobber] {
|
||||
live := analyzeLiveness(t, cfg.Arch)
|
||||
if clobbered := clobberedCalleeSaved(live, cfg.Arch); len(clobbered) > 0 {
|
||||
always, rt := clobberedGoFixed(live, cfg.Arch, reachesRuntime(t))
|
||||
if len(always) > 0 {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: t.Keyword.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeRegisterClobber,
|
||||
Message: fmt.Sprintf("callee-saved register(s) %s written but never saved/restored", strings.Join(clobbered, ", ")),
|
||||
Message: fmt.Sprintf("register(s) %s written but never saved/restored: fixed by the Go ABI (frame/goroutine pointer)", strings.Join(always, ", ")),
|
||||
})
|
||||
}
|
||||
if len(rt) > 0 {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: t.Keyword.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeRegisterClobber,
|
||||
Message: fmt.Sprintf("goroutine-pointer register(s) %s written but never saved/restored in a function that can reach the Go runtime", strings.Join(rt, ", ")),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -341,6 +350,29 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
return out
|
||||
}
|
||||
|
||||
// reachesRuntime reports whether a function can reach the Go runtime: it is
|
||||
// not NOSPLIT (so the stack-split and traceback machinery runs) or it makes a
|
||||
// CALL. Goroutine-pointer registers must survive such functions; a NOSPLIT
|
||||
// leaf may clobber them, since the ABI0 transition restores them (the
|
||||
// runtime's own assembly relies on this, e.g. R14 on amd64).
|
||||
func reachesRuntime(t *ast.Text) bool {
|
||||
nosplit := false
|
||||
for _, f := range t.Flags {
|
||||
if strings.EqualFold(f, "NOSPLIT") {
|
||||
nosplit = true
|
||||
}
|
||||
}
|
||||
for _, s := range t.Body {
|
||||
if in, ok := s.(*ast.Instr); ok {
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "CALL", "BL", "JAL": // amd64, arm64/loong64, riscv64 calls
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return !nosplit
|
||||
}
|
||||
|
||||
// usesFPArgs reports whether a function references its arguments through the FP
|
||||
// pseudo-register — i.e. it uses the stack-based ABI0 layout, where the
|
||||
// declared argument size must match the signature.
|
||||
|
||||
+5
-5
@@ -48,11 +48,11 @@ func TestFixtureIsClean(t *testing.T) {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
// The fixture mirrors the go-flac kernels, which use callee-saved registers
|
||||
// (BX, R13) without saving them; the register-clobber audit flags that by
|
||||
// design. This test targets the other rules, so the audit is disabled here
|
||||
// (it is covered by TestRegisterClobber).
|
||||
diags := File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeRegisterClobber: true}})
|
||||
// The fixture mirrors the go-flac kernels, which write the Go ABI0
|
||||
// scratch registers (BX, R13) without saving them — legal under Go's
|
||||
// stack-based ABI, so the register-clobber audit stays silent and the
|
||||
// fixture must lint entirely clean.
|
||||
diags := File(f, Config{Arch: arch.AMD64})
|
||||
if len(diags) != 0 {
|
||||
t.Fatalf("expected no diagnostics on the fixture, got %+v", diags)
|
||||
}
|
||||
|
||||
+48
-47
@@ -4,7 +4,6 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -248,7 +247,7 @@ func instrEffect(in *ast.Instr, a arch.Arch) regEffect {
|
||||
}
|
||||
|
||||
compare := isCompare(mnem)
|
||||
dstIdx := dstIndex(in, a)
|
||||
dstIdx := dstIndex(in)
|
||||
|
||||
for i, op := range in.Operands {
|
||||
r := gprName(op, a)
|
||||
@@ -281,13 +280,11 @@ func instrEffect(in *ast.Instr, a arch.Arch) regEffect {
|
||||
return eff
|
||||
}
|
||||
|
||||
// dstIndex returns the operand index of the destination register: last for the
|
||||
// Plan 9 (amd64) spelling, first for arm64/riscv64/loong64.
|
||||
func dstIndex(in *ast.Instr, a arch.Arch) int {
|
||||
if a == arch.AMD64 {
|
||||
// dstIndex returns the operand index of the destination register: in Plan 9
|
||||
// notation the destination is the last operand on every architecture Go
|
||||
// supports (amd64, arm64, riscv64 and loong64 alike).
|
||||
func dstIndex(in *ast.Instr) int {
|
||||
return len(in.Operands) - 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// isCompare reports whether the mnemonic only reads its operands (setting flags).
|
||||
@@ -372,41 +369,37 @@ func sameSet(a, b map[string]bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// calleeSavedGPRs returns the general-purpose registers an assembly function
|
||||
// must preserve for its caller, using the register names the assembler accepts
|
||||
// for each architecture.
|
||||
func calleeSavedGPRs(a arch.Arch) map[string]bool {
|
||||
// goFixedGPRs returns the general-purpose registers the Go ABI designates as
|
||||
// fixed across calls — the ones hand-written assembly must not permanently
|
||||
// clobber. This follows cmd/compile/abi-internal.md, not the platform ABI:
|
||||
// Go's stack-based ABI0 (which hand-written assembly uses) has no System V
|
||||
// style callee-saved registers, so clobbering the argument and scratch
|
||||
// registers (amd64 BX, R12, R13, R15, …) is legal.
|
||||
//
|
||||
// Two groups are returned. always holds registers whose loss is never safe.
|
||||
// runtime holds registers that survive an ABI0 leaf only because the
|
||||
// transition machinery restores them (on amd64 the g pointer is reloaded
|
||||
// from TLS): clobbering them is safe exactly in NOSPLIT functions that make
|
||||
// no calls, which is how the runtime's own assembly uses them.
|
||||
func goFixedGPRs(a arch.Arch) (always, runtime map[string]bool) {
|
||||
switch a {
|
||||
case arch.AMD64:
|
||||
return gprSet("BX", "BP", "R12", "R13", "R14", "R15")
|
||||
// BP maintains the frame chain; R14 holds the current goroutine.
|
||||
// R15 is scratch except in dynamically linked binaries, so it is not
|
||||
// flagged.
|
||||
return gprSet("BP"), gprSet("R14")
|
||||
case arch.ARM64:
|
||||
names := []string{"R29", "R30"} // FP, LR
|
||||
for i := 19; i <= 28; i++ {
|
||||
names = append(names, fmt.Sprintf("R%d", i))
|
||||
}
|
||||
return gprSet(names...)
|
||||
// R18 is reserved for the OS on some platforms, R28 holds the current
|
||||
// goroutine, R29 is the frame pointer.
|
||||
return gprSet("R18", "R28", "R29"), nil
|
||||
case arch.RISCV:
|
||||
// RA (X1) and the S registers (X8, X9, X18–X27) are callee-saved.
|
||||
names := []string{"X1", "RA", "X8", "X9", "S0", "S1", "FP"}
|
||||
for i := 18; i <= 27; i++ {
|
||||
names = append(names, fmt.Sprintf("X%d", i))
|
||||
}
|
||||
for i := 2; i <= 11; i++ {
|
||||
names = append(names, fmt.Sprintf("S%d", i))
|
||||
}
|
||||
return gprSet(names...)
|
||||
// X27 holds the current goroutine.
|
||||
return gprSet("X27"), nil
|
||||
case arch.LOONG64:
|
||||
// RA (R1), FP (R22) and S0–S8 (R23–R31) are callee-saved.
|
||||
names := []string{"R1", "RA", "R22", "FP"}
|
||||
for i := 23; i <= 31; i++ {
|
||||
names = append(names, fmt.Sprintf("R%d", i))
|
||||
// R22 holds the current goroutine.
|
||||
return gprSet("R22"), nil
|
||||
}
|
||||
for i := 0; i <= 8; i++ {
|
||||
names = append(names, fmt.Sprintf("S%d", i))
|
||||
}
|
||||
return gprSet(names...)
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func gprSet(names ...string) map[string]bool {
|
||||
@@ -417,15 +410,16 @@ func gprSet(names ...string) map[string]bool {
|
||||
return m
|
||||
}
|
||||
|
||||
// clobberedCalleeSaved returns the callee-saved registers a function writes
|
||||
// without also saving and restoring them — i.e. registers whose caller-owned
|
||||
// value is lost across the call. It walks the blocks of the liveness analysis
|
||||
// (so the control-flow graph is what supplies the instruction set) and
|
||||
// aggregates each instruction's register effects.
|
||||
func clobberedCalleeSaved(l *liveness, a arch.Arch) []string {
|
||||
callee := calleeSavedGPRs(a)
|
||||
if len(callee) == 0 {
|
||||
return nil
|
||||
// clobberedGoFixed returns the Go-ABI-fixed registers a function writes
|
||||
// without also saving and restoring them. The first result lists registers
|
||||
// whose loss is never safe; the second lists the goroutine-pointer class,
|
||||
// whose loss is reported only when reachesRuntime is true (a non-NOSPLIT
|
||||
// function, or one that makes calls — the ABI0 transition machinery restores
|
||||
// the g pointer only on such paths).
|
||||
func clobberedGoFixed(l *liveness, a arch.Arch, reachesRuntime bool) (always, runtime []string) {
|
||||
alwaysSet, runtimeSet := goFixedGPRs(a)
|
||||
if len(alwaysSet) == 0 && len(runtimeSet) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
def := map[string]bool{}
|
||||
saved := map[string]bool{}
|
||||
@@ -444,12 +438,19 @@ func clobberedCalleeSaved(l *liveness, a arch.Arch) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
clobbered := func(set map[string]bool) []string {
|
||||
var out []string
|
||||
for r := range callee {
|
||||
for r := range set {
|
||||
if def[r] && !(saved[r] && restored[r]) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
always = clobbered(alwaysSet)
|
||||
if reachesRuntime {
|
||||
runtime = clobbered(runtimeSet)
|
||||
}
|
||||
return always, runtime
|
||||
}
|
||||
|
||||
+112
-16
@@ -5,36 +5,132 @@ package lint
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestRegisterClobber detects writes to callee-saved registers that are not
|
||||
// saved and restored.
|
||||
// TestRegisterClobber checks the register-clobber audit is calibrated to the
|
||||
// Go ABI (cmd/compile/abi-internal.md), not the platform ABI: Go's
|
||||
// stack-based ABI0 — which hand-written assembly uses — has no System V
|
||||
// style callee-saved registers, so argument and scratch registers may be
|
||||
// clobbered freely. Only the registers the ABI fixes across calls (the
|
||||
// frame pointer, the goroutine pointer, OS-reserved registers) are audited.
|
||||
func TestRegisterClobber(t *testing.T) {
|
||||
// BX (callee-saved on amd64) is written but never saved → clobbered.
|
||||
clob := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
// amd64: BX, R12, R13 and R15 are argument/permanent-scratch registers in
|
||||
// Go ABI0 — writing them unsaved is legal (a System V calibration would
|
||||
// report all of these).
|
||||
scratch := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVQ CX, BX\n"+
|
||||
"\tXORL R12, R12\n"+
|
||||
"\tXORL R13, R13\n"+
|
||||
"\tXORL R15, R15\n"+
|
||||
"\tRET\n")
|
||||
if codes(clob)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved callee-saved write should be flagged: %+v", clob)
|
||||
if codes(scratch)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("Go ABI0 scratch registers must not be flagged: %+v", scratch)
|
||||
}
|
||||
|
||||
// Saved and restored → preserved.
|
||||
// amd64: R14 (the goroutine pointer) in a NOSPLIT function without calls
|
||||
// is the runtime's own pattern — the ABI0 transition restores it — so it
|
||||
// is not flagged.
|
||||
leaf := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tXORL R14, R14\n"+
|
||||
"\tRET\n")
|
||||
if codes(leaf)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("R14 in a NOSPLIT leaf must not be flagged: %+v", leaf)
|
||||
}
|
||||
|
||||
// amd64: R14 in a function that makes a call is a genuine hazard.
|
||||
withCall := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tXORL R14, R14\n"+
|
||||
"\tCALL ·g(SB)\n"+
|
||||
"\tRET\n")
|
||||
if codes(withCall)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved R14 with a call should be flagged: %+v", withCall)
|
||||
}
|
||||
|
||||
// amd64: R14 in a non-NOSPLIT function is a hazard regardless of calls.
|
||||
split := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), $0\n"+
|
||||
"\tMOVQ CX, R14\n"+
|
||||
"\tRET\n")
|
||||
if codes(split)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved R14 in a non-NOSPLIT function should be flagged: %+v", split)
|
||||
}
|
||||
|
||||
// amd64: R14 saved and restored around the call is preserved.
|
||||
saved := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $8\n"+
|
||||
"\tPUSHQ BX\n"+
|
||||
"\tMOVQ CX, BX\n"+
|
||||
"\tPOPQ BX\n"+
|
||||
"\tPUSHQ R14\n"+
|
||||
"\tXORL R14, R14\n"+
|
||||
"\tCALL ·g(SB)\n"+
|
||||
"\tPOPQ R14\n"+
|
||||
"\tRET\n")
|
||||
if codes(saved)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("saved/restored register must not be flagged: %+v", saved)
|
||||
t.Fatalf("saved/restored R14 must not be flagged: %+v", saved)
|
||||
}
|
||||
|
||||
// A caller-saved register (CX) is fine to write.
|
||||
caller := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
// amd64: BP maintains the frame chain and is always audited.
|
||||
bp := lintSrc(t, "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVQ $1, CX\n"+
|
||||
"\tMOVQ CX, BP\n"+
|
||||
"\tRET\n")
|
||||
if codes(caller)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("caller-saved register must not be flagged: %+v", caller)
|
||||
if codes(bp)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved BP write should be flagged: %+v", bp)
|
||||
}
|
||||
|
||||
// arm64: R20 is scratch; R28 (goroutine pointer) and R18 (OS-reserved)
|
||||
// are fixed by the Go ABI.
|
||||
armScratch := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVD R0, R20\n"+
|
||||
"\tRET\n")
|
||||
if codes(armScratch)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("arm64 scratch register must not be flagged: %+v", armScratch)
|
||||
}
|
||||
armG := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVD R0, R28\n"+
|
||||
"\tRET\n")
|
||||
if codes(armG)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved arm64 R28 write should be flagged: %+v", armG)
|
||||
}
|
||||
armReserved := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVD R0, R18\n"+
|
||||
"\tRET\n")
|
||||
if codes(armReserved)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("arm64 R18 write should be flagged: %+v", armReserved)
|
||||
}
|
||||
|
||||
// riscv64: X27 holds the goroutine; X5–X7 are scratch.
|
||||
riscScratch := lintSrcArch(t, "t_riscv64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOV X5, X6\n"+
|
||||
"\tRET\n")
|
||||
if codes(riscScratch)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("riscv64 scratch register must not be flagged: %+v", riscScratch)
|
||||
}
|
||||
riscG := lintSrcArch(t, "t_riscv64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOV X5, X27\n"+
|
||||
"\tRET\n")
|
||||
if codes(riscG)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved riscv64 X27 write should be flagged: %+v", riscG)
|
||||
}
|
||||
|
||||
// loong64: R22 holds the goroutine; R5–R19 are argument/scratch.
|
||||
loongScratch := lintSrcArch(t, "t_loong64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVV R5, R6\n"+
|
||||
"\tRET\n")
|
||||
if codes(loongScratch)[CodeRegisterClobber] != 0 {
|
||||
t.Fatalf("loong64 scratch register must not be flagged: %+v", loongScratch)
|
||||
}
|
||||
loongG := lintSrcArch(t, "t_loong64.s", "#include \"textflag.h\"\n"+
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||||
"\tMOVV R5, R22\n"+
|
||||
"\tRET\n")
|
||||
if codes(loongG)[CodeRegisterClobber] != 1 {
|
||||
t.Fatalf("unsaved loong64 R22 write should be flagged: %+v", loongG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user