fix(asm): enforce the arm64 TLBI, RPRFM, FCVT and integer-pair arities
Assisted-by: GLM 5.3
This commit is contained in:
1 parent
7a69be8b59
commit
33e7fdac98
4 files changed
+140
-33
No files matched your search
+71
-7
@@ -3448,6 +3448,34 @@ func arm64SimdNarrowPair(mnem, src, dst string, two bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// arm64SimdFCVTLongPair validates the FCVTL width pair: S to D alone, with
|
||||
// the plain spelling reading S2 and the .2 spelling S4, the destination
|
||||
// always D2.
|
||||
func arm64SimdFCVTLongPair(mnem, src, dst string, two bool) error {
|
||||
wantSrc := "S2"
|
||||
if two {
|
||||
wantSrc = "S4"
|
||||
}
|
||||
if src != wantSrc || dst != "D2" {
|
||||
return fmt.Errorf("%s: operand mismatch for the %s spelling: want %s, %s", mnem, mnem, wantSrc, "D2")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// arm64SimdFCVTNarrowPair validates the FCVTN width pair: D to S alone, the
|
||||
// source always D2, the destination S2 for the plain spelling and S4 for
|
||||
// the .2 spelling.
|
||||
func arm64SimdFCVTNarrowPair(mnem, src, dst string, two bool) error {
|
||||
wantDst := "S2"
|
||||
if two {
|
||||
wantDst = "S4"
|
||||
}
|
||||
if src != "D2" || dst != wantDst {
|
||||
return fmt.Errorf("%s: operand mismatch for the %s spelling: want %s, %s", mnem, mnem, "D2", wantDst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// arm64SimdNLArrBits returns the arrangement bits a narrow/long/wide
|
||||
// instruction contributes: the driving arrangement's size and Q bits, for
|
||||
// the FCVT family only the Q bit (whose size field is fixed in the base),
|
||||
@@ -3504,7 +3532,19 @@ func encodeARM64SimdNL(mnem string, spec a64SimdNLSpec, ops []*ast.Operand) ([]b
|
||||
}
|
||||
var drive string
|
||||
var pairErr error
|
||||
if spec.form == a64NLTwoNarrow {
|
||||
if spec.qonly {
|
||||
// The FCVT conversions are pinned to one width pair: S to D for
|
||||
// the lengthening (FCVTL S2→D2, FCVTL2 S4→D2), D to S for the
|
||||
// narrowing (FCVTN D2→S2, FCVTN2 D2→S4); the size field is
|
||||
// fixed in the opcode and no other arrangement exists.
|
||||
if spec.form == a64NLTwoNarrow {
|
||||
drive = vd.arr
|
||||
pairErr = arm64SimdFCVTNarrowPair(mnem, vn.arr, vd.arr, two)
|
||||
} else {
|
||||
drive = vn.arr
|
||||
pairErr = arm64SimdFCVTLongPair(mnem, vn.arr, vd.arr, two)
|
||||
}
|
||||
} else if spec.form == a64NLTwoNarrow {
|
||||
// XTN/FCVTN: wide source into a narrow destination; the
|
||||
// arrangement bits follow the destination.
|
||||
drive, pairErr = vd.arr, arm64SimdNarrowPair(mnem, vn.arr, vd.arr, two)
|
||||
@@ -3863,7 +3903,9 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi
|
||||
}
|
||||
}
|
||||
// The FP pairs take FP registers against a GP base (asm7.go: invalid
|
||||
// register pair).
|
||||
// register pair). The integer pairs are the mirror image: an F pair on
|
||||
// LDP/STP is an invalid register pair too, the toolchain having no class
|
||||
// for it.
|
||||
if strings.HasPrefix(mnem, "FLDP") || strings.HasPrefix(mnem, "FSTP") {
|
||||
first := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(pairOp.Raw), "("))
|
||||
if i := strings.IndexAny(first, ",)"); i >= 0 {
|
||||
@@ -3875,6 +3917,14 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi
|
||||
if strings.HasPrefix(strings.TrimLeft(operandRegName(memOp), "( "), "F") {
|
||||
return nil, fmt.Errorf("%s: invalid register pair: the base must be a general register", mnem)
|
||||
}
|
||||
} else {
|
||||
first := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(pairOp.Raw), "("))
|
||||
if i := strings.IndexAny(first, ",)"); i >= 0 {
|
||||
first = strings.TrimSpace(first[:i])
|
||||
}
|
||||
if strings.HasPrefix(first, "F") {
|
||||
return nil, fmt.Errorf("%s: invalid register pair %s", mnem, pairOp.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// Pair access against a static symbol: ADRP R27, sym; ADD R27, R27, #lo;
|
||||
@@ -4192,19 +4242,28 @@ func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) {
|
||||
w := 0xd5080000 | inst.op1<<16 | 7<<12 | inst.cm<<8 | inst.op2<<5
|
||||
return a64wordLE(w | uint32(rn)&31), nil
|
||||
case "TLBI":
|
||||
// The register operand is optional: TLBI VMALLE1IS alone means ZR.
|
||||
// The register operand's arity follows the operation (asm7.go's
|
||||
// sysInstFields): the by-address spellings take the Xt and the
|
||||
// whole-entry ones (VMALL*, ALL**) take none, an explicit register
|
||||
// being extraneous.
|
||||
if len(ops) != 1 && len(ops) != 2 {
|
||||
return nil, fmt.Errorf("TLBI expects <op>[, Rn]")
|
||||
}
|
||||
inst, ok := a64TLBIOps[operandRegName(ops[0])]
|
||||
name := operandRegName(ops[0])
|
||||
inst, ok := a64TLBIOps[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("TLBI: unknown operation %q", operandRegName(ops[0]))
|
||||
return nil, fmt.Errorf("TLBI: unknown operation %q", name)
|
||||
}
|
||||
rt := 31
|
||||
if len(ops) == 2 {
|
||||
if !arm64TLBITakesReg(name) {
|
||||
return nil, fmt.Errorf("TLBI %s: extraneous register at operand 2", name)
|
||||
}
|
||||
if rt = arm64RegNum(operandRegName(ops[1])); rt < 0 {
|
||||
return nil, fmt.Errorf("TLBI: invalid register operand")
|
||||
}
|
||||
} else if arm64TLBITakesReg(name) {
|
||||
return nil, fmt.Errorf("TLBI %s: missing register at operand 2", name)
|
||||
}
|
||||
w := 0xd5080000 | inst.op1<<16 | 8<<12 | inst.cm<<8 | inst.op2<<5
|
||||
return a64wordLE(w | uint32(rt)&31), nil
|
||||
@@ -4328,6 +4387,11 @@ func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) {
|
||||
if rm < 0 {
|
||||
return nil, fmt.Errorf("RPRFM: invalid register operand")
|
||||
}
|
||||
// The toolchain's class ladder takes a plain R register alone: RSP
|
||||
// and ZR are illegal combinations (asm7.go case 110).
|
||||
if name := operandRegName(ops[1]); strings.EqualFold(name, "RSP") || strings.EqualFold(name, "SP") || strings.EqualFold(name, "ZR") {
|
||||
return nil, fmt.Errorf("RPRFM: illegal combination: %s is not a general register", name)
|
||||
}
|
||||
var op uint64
|
||||
if isImmOperand(ops[2]) {
|
||||
op = uint64(arm64Imm64(ops[2]))
|
||||
@@ -5015,8 +5079,8 @@ func encodeARM64VLDST(mnem string, post uint32, ops []*ast.Operand) ([]byte, err
|
||||
}
|
||||
// The toolchain's addressing contract for the structure forms (asm7.go's
|
||||
// class ladder): an index register exists only as the post-index
|
||||
// increment, so it is illegal without .P and takes neither an offset nor
|
||||
// an extend nor a shift beside it.
|
||||
// increment, so it is illegal without .P, and the immediate increment
|
||||
// must be exactly the bytes the whole list transfers.
|
||||
idx := ops[memIdx].Addr.Index
|
||||
if idx != "" && post == 0 {
|
||||
return nil, fmt.Errorf("%s: illegal combination: the register index is a post-index, it needs the .P spelling", mnem)
|
||||
|
||||
@@ -2363,6 +2363,56 @@ func TestArm64VLDSTPostIndexContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64OperandArities pins the operand-shape rules the error corpus
|
||||
// drove home: the TLBI register arity follows the operation, RPRFM's second
|
||||
// operand is a plain register, the FCVT conversions are pinned to their one
|
||||
// width pair, and the integer pairs take integer registers alone.
|
||||
func TestArm64OperandArities(t *testing.T) {
|
||||
accept := []string{
|
||||
"\tTLBI\tVAE1, R1\n",
|
||||
"\tTLBI\tVMALLE1IS\n",
|
||||
"\tTLBI\tALLE3OS\n",
|
||||
"\tRPRFM\t(R1), R2, PLDKEEP\n",
|
||||
"\tRPRFM\t(R1), R27, PLDKEEP\n",
|
||||
"\tVFCVTL\tV1.S2, V2.D2\n",
|
||||
"\tVFCVTL2\tV1.S4, V2.D2\n",
|
||||
"\tVFCVTN\tV1.D2, V2.S2\n",
|
||||
"\tVFCVTN2\tV1.D2, V2.S4\n",
|
||||
"\tLDP\t(R0), (R0, R1)\n",
|
||||
}
|
||||
reject := []string{
|
||||
"\tTLBI\tVMALLE1IS, R0\n", // whole-entry op: extraneous register
|
||||
"\tTLBI\tALLE3OS, ZR\n", // ditto, the ZR spelling included
|
||||
"\tTLBI\tVAE1IS\n", // by-address op: missing register
|
||||
"\tTLBI\tRVALE3\n", // ditto
|
||||
"\tRPRFM\t(R1), RSP, PLDKEEP\n",
|
||||
"\tRPRFM\t(R1), ZR, PLDKEEP\n",
|
||||
"\tVFCVTL\tV1.H4, V2.S4\n", // no half-precision conversion
|
||||
"\tVFCVTN\tV1.D2, V2.H4\n", // ditto on the narrowing side
|
||||
"\tLDP\t(R0), (F0, F1)\n", // FP pair on the integer mnemonic
|
||||
"\tSTP\t(F2, F3), (R0)\n", // ditto
|
||||
}
|
||||
for _, src := range accept {
|
||||
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n")
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("expected acceptance for %q, got parse rejection", strings.TrimSpace(src))
|
||||
continue
|
||||
}
|
||||
if _, err := AssembleFileARM64(f); err != nil {
|
||||
t.Errorf("expected acceptance for %q, got %v", strings.TrimSpace(src), err)
|
||||
}
|
||||
}
|
||||
for _, src := range reject {
|
||||
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n")
|
||||
if len(errs) > 0 {
|
||||
continue // a parse rejection is a rejection
|
||||
}
|
||||
if _, err := AssembleFileARM64(f); err == nil {
|
||||
t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64FPImmediate pins the FP immediate moves against `go tool asm`
|
||||
// words: the FMOV (immediate) instruction for the 8-bit encodable values and
|
||||
// the FMOV-from-ZR move for zero, plus the rejections the toolchain raises
|
||||
|
||||
@@ -15,28 +15,15 @@ import (
|
||||
// arm64AcceptedErrorShapes lists the toolchain's arm64error.s spellings gasm
|
||||
// still accepts, each an acceptance superset with a known shape. The list
|
||||
// only shrinks: every tightening of the encoder moves spellings out of it,
|
||||
// and a spelling reappearing here means a regression.
|
||||
var arm64AcceptedErrorShapes = []string{
|
||||
// spelling, the .P immediate against the aggregate register size, the
|
||||
// REGTMP (R27) as an explicit operand the toolchain refuses, the pair
|
||||
// split paths included.
|
||||
// TLBI operand arity and the range-prefetch operand shapes.
|
||||
"TLBI VMALLE1IS, R0",
|
||||
"TLBI ALLE3OS, ZR",
|
||||
"TLBI VAE1IS",
|
||||
"TLBI RVALE3",
|
||||
"RPRFM (R1), RSP, PLDKEEP",
|
||||
// The GP register pairs on the FP mnemonics, the other way round.
|
||||
"LDP (R0), (F0, F1)",
|
||||
"STP (F2, F3), (R0)",
|
||||
// FCVTL exists for the S to D pair alone.
|
||||
"VFCVTL V1.H4, V2.S4",
|
||||
}
|
||||
// and a spelling reappearing here means a regression. The catalogue is
|
||||
// empty as of Go 1.27: every line the toolchain's corpus rejects, gasm
|
||||
// rejects too.
|
||||
var arm64AcceptedErrorShapes = []string{}
|
||||
|
||||
// TestArm64ToolchainErrorParity walks the toolchain's arm64error.s (Go
|
||||
// 1.27, arm64) and requires gasm to reject every case the toolchain rejects,
|
||||
// the documented acceptance supersets above excepted. A live Go toolchain
|
||||
// is needed for the source file; the test skips without one or in -short.
|
||||
// 1.27, arm64) and requires gasm to reject every case the toolchain rejects.
|
||||
// A live Go toolchain is needed for the source file; the test skips without
|
||||
// one or in -short.
|
||||
func TestArm64ToolchainErrorParity(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live arm64error.s corpus: skipped in -short mode")
|
||||
@@ -65,12 +52,6 @@ func TestArm64ToolchainErrorParity(t *testing.T) {
|
||||
}
|
||||
body = strings.ReplaceAll(body, "\t", " ")
|
||||
body = strings.Join(strings.Fields(body), " ")
|
||||
// Label-relative and non-operand lines need their function context.
|
||||
if strings.Contains(body, "(PC)") || strings.Contains(body, "(SB)") || strings.Contains(body, "PCALIGN") ||
|
||||
strings.HasPrefix(body, "RET") || strings.HasPrefix(body, "NOP") || strings.HasPrefix(body, "BRK") ||
|
||||
strings.Contains(body, "again") || strings.Contains(body, "next") || strings.Contains(body, "loop") {
|
||||
continue
|
||||
}
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||||
f, perr := parser.Parse("errorparity.s", src)
|
||||
if len(perr) > 0 {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
package asm
|
||||
|
||||
import "strings"
|
||||
|
||||
// arm64 system registers and system-instruction aliases.
|
||||
//
|
||||
// The tables are transcribed from the data the Go toolchain itself carries
|
||||
@@ -546,6 +548,16 @@ var a64TLBIOps = map[string]a64SysInst{
|
||||
"VMALLS12E1OS": {0x4, 0x1, 0x6},
|
||||
}
|
||||
|
||||
// arm64TLBITakesReg reports whether a TLBI operation spells a by-address
|
||||
// invalidation that carries the address in its optional second register
|
||||
// (asm7.go's sysInstFields hasOperand2). The whole-entry spellings are
|
||||
// exactly the VMALL-prefixed and the ALL-prefixed ones; everything else
|
||||
// (VAE*, VAAE*, VALE*, RVAA*, RVAE*, ASIDE1*, IPAS2*, RIPAS2*) is
|
||||
// by-address and takes the register.
|
||||
func arm64TLBITakesReg(name string) bool {
|
||||
return !strings.HasPrefix(name, "VMALL") && !strings.HasPrefix(name, "ALL")
|
||||
}
|
||||
|
||||
// a64DCOps2 maps the DC operation names to their fields; the register
|
||||
// operand is mandatory.
|
||||
var a64DCOps2 = map[string]a64SysInst{
|
||||
|
||||
Reference in new issue
Block a user