diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index add36b5..ce42aa5 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -287,6 +287,9 @@ func arm64InstrSize(instr *ast.Instr, fi arm64FrameInfo, pos int) int { if mnem == "BYTE" { return len(ops) } + if mnem == "NOP" { + return 0 // the zero-size pseudo-instruction, operand or not + } switch mnem { case "VMOVS", "VMOVD", "VMOVQ": // ADRP + ADD + wide load against a pooled literal. @@ -414,7 +417,33 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64 switch mnem { case "RET": return arm64RetInstr(fi, ops, relocs), nil - case "NOP", "NOOP": + case "NOP": + // The toolchain's ANOP is a zero-size pseudo-instruction: no bytes + // whatever operand rides it, the immediate, register and vector + // classes included (asm7.go's ANOP rows), and anything else is an + // illegal combination. A bare register operand parses as a symbol + // reference with no base, the way operandRegName reads it. + if len(ops) > 1 { + return nil, fmt.Errorf("NOP: illegal combination") + } + if len(ops) == 1 && !isImmOperand(ops[0]) { + op := ops[0] + name := operandRegName(op) + bare := op.Addr.Base == "" && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" && + !op.Addr.HasOff && op.Addr.Index == "" + reg := arm64RegNum(name) >= 0 || + len(name) > 1 && (name[0] == 'F' || name[0] == 'V') && strings.Trim(name[1:], "0123456789") == "" + if !bare || !reg { + return nil, fmt.Errorf("NOP: illegal combination") + } + } + return nil, nil + case "NOOP": + // NOOP is the real hint instruction: one word, and it takes no + // operand at all. + if len(ops) != 0 { + return nil, fmt.Errorf("NOOP: illegal combination") + } return a64wordLE(a64NOP), nil case "UNDEF": return a64wordLE(a64BRK(0)), nil diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 86cdbb0..5ccf0a2 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -568,10 +568,11 @@ func TestArm64CondCompare(t *testing.T) { // TestArm64CompareBranch pins CBZ/CBNZ/TBZ/TBNZ against a label five and // six words ahead, matching go tool asm's own offsets. func TestArm64CompareBranch(t *testing.T) { - // Layout: CBZ(0) TBZ(4) TBNZ(8) CBNZ(12) NOP(16) NOP(17th word...) done. + // Layout: CBZ(0) TBZ(4) TBNZ(8) CBNZ(12) NOOP(16) NOOP(17th word...) done. + // The fillers are NOOP, not NOP: the toolchain's NOP emits nothing. src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n" + "\tCBZ R1, done\n\tTBZ $4, R7, done\n\tTBNZ $33, R7, done\n\tCBNZW R2, done\n" + - "\tNOP\n\tNOP\n\tdone:\tNOP\n\tRET\n" + "\tNOOP\n\tNOOP\n\tdone:\tNOOP\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) @@ -604,7 +605,7 @@ func TestArm64CompareBranch(t *testing.T) { // TestArm64ADR pins ADR against a forward label. func TestArm64ADR(t *testing.T) { src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n" + - "\tADR done, R10\n\tNOP\n\tNOP\n\tdone:\tNOP\n\tRET\n" + "\tADR done, R10\n\tNOOP\n\tNOOP\n\tdone:\tNOOP\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) @@ -2234,6 +2235,38 @@ func TestArm64LiteralPoolGuard(t *testing.T) { } } +// TestArm64NoopVsNop pins the two spellings apart, the way the toolchain +// holds them: NOOP is the real hint instruction and NOP a zero-size pseudo +// whose operand, when it is an immediate or a register, rides along for go +// vet's benefit; every other operand shape and the NOOP operands are illegal +// combinations. +func TestArm64NoopVsNop(t *testing.T) { + got := arm64Words(t, "\tNOP\n\tNOOP\n\tNOP\tR0\n") + want := []uint32{0xd503201f, 0xd65f03c0} + if len(got) != len(want) { + t.Fatalf("word count = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) + } + } + for _, src := range []string{ + "\tNOOP\t$0\n", + "\tNOOP\tR0\n", + "\tNOP\t8(R0)\n", + "\tNOP\t$0, $1\n", + } { + 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