// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "testing" "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) func TestArm64LDRSTREncoding(t *testing.T) { tests := []struct { name string got uint32 want uint32 }{ {"LDR X4, [SP, #56]", a64LSU(3, 0, 1, 7, 31, 4), 0xf9401fe4}, {"STR X4, [SP, #64]", a64LSU(3, 0, 0, 8, 31, 4), 0xf90023e4}, {"STR X5, [SP, #32]", a64LSU(3, 0, 0, 4, 31, 5), 0xf90013e5}, {"LDR X6, [SP, #32]", a64LSU(3, 0, 1, 4, 31, 6), 0xf94013e6}, } for _, tt := range tests { if tt.got != tt.want { t.Errorf("%s: got %08x, want %08x", tt.name, tt.got, tt.want) } } } func TestArm64PrologueEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false} pro := arm64Prologue(fi) if len(pro) != 12 { t.Fatalf("prologue length: got %d, want 12", len(pro)) } expected := []uint32{0xf81d0ffe, 0xf81f83fd, 0xd10023fd} for i, w := range leWords(pro) { if w != expected[i] { t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i]) } } } func TestArm64EpilogueSmallEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false} ret := arm64Return(fi) if len(ret) != 12 { t.Fatalf("epilogue length: got %d, want 12", len(ret)) } // Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #48; RET expected := []uint32{0xf85f83fd, 0xf84307fe, 0xd65f03c0} for i, w := range leWords(ret) { if w != expected[i] { t.Errorf("epilogue word %d: got %08x, want %08x", i, w, expected[i]) } } } func TestArm64LargeFrameEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 272, frame: 256, leaf: false} pro := arm64Prologue(fi) if len(pro) != 16 { t.Fatalf("prologue length: got %d, want 16", len(pro)) } expected := []uint32{0xd10443f4, 0xa93ffa9d, 0x9100029f, 0xd10023fd} for i, w := range leWords(pro) { if w != expected[i] { t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i]) } } epi := arm64Return(fi) if len(epi) != 12 { t.Fatalf("epilogue length: got %d, want 12", len(epi)) } eexpected := []uint32{0xa97ffbfd, 0x910443ff, 0xd65f03c0} for i, w := range leWords(epi) { if w != eexpected[i] { t.Errorf("epilogue word %d: got %08x, want %08x", i, w, eexpected[i]) } } } func TestArm64NoFrame(t *testing.T) { fi := arm64FrameInfo{autosize: 0, frame: 0, leaf: true} pro := arm64Prologue(fi) if len(pro) != 0 { t.Errorf("no-frame prologue: got %d bytes, want 0", len(pro)) } ret := arm64Return(fi) if len(ret) != 4 { t.Fatalf("no-frame return: got %d bytes, want 4", len(ret)) } if leWord(ret) != 0xd65f03c0 { t.Errorf("no-frame RET: got %08x, want d65f03c0", leWord(ret)) } } func TestArm64RegNum(t *testing.T) { tests := []struct { name string want int }{ {"R0", 0}, {"R4", 4}, {"R29", 29}, {"R30", 30}, {"R31", 31}, {"FP", 29}, {"LR", 30}, {"LINK", 30}, {"SP", 31}, {"ZR", 31}, {"F0", 0}, {"F4", 4}, {"F31", 31}, {"INVALID", -1}, {"X0", -1}, {"", -1}, } for _, tt := range tests { got := arm64RegNum(tt.name) if got != tt.want { t.Errorf("arm64RegNum(%q) = %d, want %d", tt.name, got, tt.want) } } } func TestArm64ComputeFrame(t *testing.T) { src := "TEXT ·f(SB), NOSPLIT, $32-0\n\tADD\tR4, R5\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } fi := arm64ComputeFrame(f.Decls[0].(*ast.Text)) if fi.frame != 32 { t.Errorf("frame: got %d, want 32", fi.frame) } if fi.autosize != 48 { // 32+8=40, aligned to48 t.Errorf("autosize: got %d, want 48", fi.autosize) } // ADD + RET with no CALL/BL → leaf if !fi.leaf { t.Error("expected leaf") } } func TestArm64IsLeaf(t *testing.T) { src := "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR4, R5\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if !arm64IsLeaf(f.Decls[0].(*ast.Text)) { t.Error("expected leaf") } src2 := "TEXT ·f(SB), NOSPLIT, $0-0\n\tBL\tother(SB)\n\tRET\n" f2, errs := parser.Parse("test_arm64.s", src2) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if arm64IsLeaf(f2.Decls[0].(*ast.Text)) { t.Error("expected non-leaf") } } func TestArm64Bitmask(t *testing.T) { tests := []struct { v uint64 sf int N, immr, imms uint32 ok bool }{ {1, 1, 1, 0, 0, true}, // single bit at pos 0 {2, 1, 1, 63, 0, true}, // single bit at pos 1 (immr = esize-1) {0, 1, 0, 0, 0, false}, // zero is not a bitmask {0xFFFFFFFFFFFFFFFF, 1, 0, 0, 0, false}, // all ones is not a bitmask {0x5555555555555555, 1, 0, 0, 0x3E, true}, // alternating bits (esize=2, ones=1) {0xFFFFFFFF00000000, 1, 1, 32, 31, true}, // upper 32 bits set (esize=64, ones=32) } for _, tt := range tests { N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf) if ok != tt.ok { t.Errorf("arm64Bitmask(%#x, %d): ok=%v, want %v", tt.v, tt.sf, ok, tt.ok) continue } if ok && (N != tt.N || immr != tt.immr || imms != tt.imms) { t.Errorf("arm64Bitmask(%#x, %d): N=%d immr=%d imms=%d, want N=%d immr=%d imms=%d", tt.v, tt.sf, N, immr, imms, tt.N, tt.immr, tt.imms) } } } func TestArm64AssembleFile(t *testing.T) { src := `#include "textflag.h" TEXT ·simple(SB), NOSPLIT, $0-0 MOV R4, R5 ADD R4, R5, R6 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if len(img.Funcs) != 1 { t.Fatalf("got %d funcs, want 1", len(img.Funcs)) } fn := img.Funcs[0] if fn.Name != "simple" { t.Errorf("func name: got %q, want %q", fn.Name, "simple") } //3 instructions ×4 bytes =12 if fn.Size != 12 { t.Errorf("func size: got %d, want 12", fn.Size) } } func TestArm64AssembleFileWithFrame(t *testing.T) { src := `#include "textflag.h" TEXT ·framed(SB), NOSPLIT, $16-8 MOVD arg+0(FP), R4 ADD $1, R4, R4 MOVD R4, ret+0(FP) RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if len(img.Funcs) != 1 { t.Fatalf("got %d funcs, want 1", len(img.Funcs)) } fn := img.Funcs[0] if fn.Frame != 16 { t.Errorf("frame: got %d, want 16", fn.Frame) } // Prologue (3×4=12) + body (3×4=12) + RET epilogue (3×4=12) = 36 if fn.Size != 36 { t.Errorf("func size: got %d, want 36", fn.Size) } } func TestArm64AssembleFileWithBranches(t *testing.T) { src := `#include "textflag.h" TEXT ·branch(SB), NOSPLIT, $0-0 BEQ done BNE skip skip: ADD R4, R5 done: RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } fn := img.Funcs[0] if fn.Size != 16 { t.Errorf("func size: got %d, want 16", fn.Size) } } func TestArm64AssembleFileWithJumpChain(t *testing.T) { src := `#include "textflag.h" TEXT ·chain(SB), NOSPLIT, $0-0 BNE skip ADD R4, R5 RET skip: B target target: ADD R6, R7 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } // BNE should be redirected past skip→target to target directly. if img.Funcs[0].Size != 24 { t.Errorf("func size: got %d, want 24", img.Funcs[0].Size) } } func TestArm64AssembleErrors(t *testing.T) { tests := []struct { name string src string }{ {"unsupported", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4, R5\n\tRET\n"}, {"undefined label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f, errs := parser.Parse("test_arm64.s", tt.src) if len(errs) > 0 { return // parse error, that's fine } _, err := AssembleFileARM64(f) if err == nil { t.Error("expected error, got nil") } }) } } func TestArm64Movcon(t *testing.T) { tests := []struct { v int64 want int }{ {0, 0}, // 0 fits at shift 0 {1, 0}, // single bit at shift 0 {0x10000, 16}, // single bit at shift 16 {0x100000000, 32}, // single bit at shift 32 {0xFF, 0}, // 0xFF fits at shift 0 {0x12345, -1}, // multiple chunks, not movcon } for _, tt := range tests { got := arm64Movcon(tt.v) if got != tt.want { t.Errorf("arm64Movcon(%#x) = %d, want %d", tt.v, got, tt.want) } } } func TestArm64RegClassOf(t *testing.T) { if arm64RegClassOf("R4") != arm64ClsGR { t.Error("R4 should be GR") } if arm64RegClassOf("F4") != arm64ClsFP { t.Error("F4 should be FP") } if arm64RegClassOf("") != arm64ClsNone { t.Error("empty should be None") } } func TestArm64ResolvePseudo(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32} // FP: offset = sym.Offset + autosize +8 base, off := arm64ResolvePseudo(&ast.Symbol{Pseudo: "FP", Offset: 0}, fi) if base != 31 || off != 56 { t.Errorf("FP: base=%d off=%d, want 31, 56", base, off) } // SP: offset = sym.Offset + frame +8 base, off = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SP", Offset: -8}, fi) if base != 31 || off != 32 { t.Errorf("SP: base=%d off=%d, want 31, 32", base, off) } // SB: unresolved base, _ = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SB"}, fi) if base != -1 { t.Errorf("SB: base=%d, want -1", base) } } // TestArm64FPSel tests FP conditional select encoding. func TestArm64FPSel(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 FCSELD GE, F10, F11, F12 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } // FCSELD should be 4 bytes + RET 4 bytes = 8 if img.Funcs[0].Size != 8 { t.Errorf("size: got %d, want 8", img.Funcs[0].Size) } } // TestArm64FPCvt tests FP conversion encoding. func TestArm64FPCvt(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 FCVTZSD F4, R0 SCVTFD R4, F8 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if img.Funcs[0].Size != 12 { t.Errorf("size: got %d, want 12", img.Funcs[0].Size) } } // TestArm64CSEL tests conditional select encoding. func TestArm64CSEL(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 CSEL EQ, R0, R1, R2 CSET NE, R3 CINC GE, R4, R5 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if img.Funcs[0].Size != 16 { t.Errorf("size: got %d, want 16", img.Funcs[0].Size) } } // TestArm64CRC32 tests CRC32 encoding. func TestArm64CRC32(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 CRC32B R0, R2 CRC32W R6, R8 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if img.Funcs[0].Size != 12 { t.Errorf("size: got %d, want 12", img.Funcs[0].Size) } } // TestArm64Bitfield tests bitfield/shift encoding. func TestArm64Bitfield(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 ASR $4, R0, R1 LSL $12, R4, R5 EXTR $8, R0, R1, R2 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if img.Funcs[0].Size != 16 { t.Errorf("size: got %d, want 16", img.Funcs[0].Size) } } // TestArm64SIMD tests SIMD encoding (via the instruction table). func TestArm64SIMD(t *testing.T) { // Verify SIMD instructions are in the table. for _, mnem := range []string{"VADD", "VSUB", "VMUL"} { if _, ok := a64InstrTable[mnem]; !ok { t.Errorf("%s not in instruction table", mnem) } } } // TestArm64LoadImm64 tests 64-bit immediate loading. func TestArm64LoadImm64(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 MOVD $0x123456789ABCDEF0, R0 MOVD $0, R1 MOVD $1, R2 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } // $0x123456789ABCDEF0 needs 4 MOVZ/MOVK instructions (16 bytes) // $0 is 1 instruction (4 bytes) // $1 is 1 bitmask instruction (4 bytes) // RET is 1 instruction (4 bytes) if img.Funcs[0].Size != 28 { t.Errorf("size: got %d, want 28", img.Funcs[0].Size) } } // TestArm64BranchCond tests conditional branch encoding. func TestArm64BranchCond(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 BEQ done BNE done BGE done BLT done ADD R4, R5 done: RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } // 4 branches + 1 ADD + 1 RET = 24 bytes if img.Funcs[0].Size != 24 { t.Errorf("size: got %d, want 24", img.Funcs[0].Size) } } // TestArm64Errors tests error paths. func TestArm64Errors(t *testing.T) { tests := []struct { name string src string }{ {"bad mnemonic", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4\n\tRET\n"}, {"bad label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"}, {"bad register", "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR99, R0\n\tRET\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f, errs := parser.Parse("test_arm64.s", tt.src) if len(errs) > 0 { return } _, err := AssembleFileARM64(f) if err == nil { t.Error("expected error, got nil") } }) } } // leWord reads a little-endian uint32 from b. func leWord(b []byte) uint32 { return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 } // leWords reads all little-endian uint32s from b. func leWords(b []byte) []uint32 { n := len(b) / 4 w := make([]uint32, n) for i := range w { w[i] = leWord(b[i*4:]) } return w } // TestArm64IndirectBranch pins the indirect branch forms in a leaf function: // JMP (Rn) lowers to BR Rn, matching the toolchain's spelling, and the raw // BR/BLR mnemonics encode directly (a gasm superset the toolchain's front // end does not accept). CALL (Rn) shares the BLR path and its non-leaf // prologue parity is covered by the ground-truth kernel. func TestArm64IndirectBranch(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 JMP (R0) BR R5 BLR R6 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } want := []uint32{ 0xd61f0000, // BR R0 0xd61f00a0, // BR R5 0xd63f00c0, // BLR R6 0xd65f03c0, // RET (BR LR) } got := leWords(img.Code) 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]) } } } // arm64Words assembles a single NOSPLIT leaf body and returns its words. func arm64Words(t *testing.T, body string) []uint32 { t.Helper() f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } return leWords(img.Code) } // TestArm64ShiftEncodings pins the shift words against `go tool asm -S` // output (Go 1.27, arm64): immediate forms alias SBFM/UBFM with ROR as EXTR, // register forms are the two-source LSLV/LSRV/ASRV/RORV. func TestArm64ShiftEncodings(t *testing.T) { got := arm64Words(t, "\tLSL $4, R0, R1\n\tLSR $8, R0, R2\n\tASR $4, R0, R3\n\tROR $12, R0, R4\n"+ "\tLSLW $4, R0, R5\n\tLSRW $8, R0, R6\n\tASRW $4, R0, R7\n\tRORW $12, R0, R8\n") want := []uint32{ 0xd37cec01, // LSL $4 = UBFM X1, X0, #60, #59 0xd348fc02, // LSR $8 = UBFM X2, X0, #8, #63 0x9344fc03, // ASR $4 = SBFM X3, X0, #4, #63 0x93c03004, // ROR $12 = EXTR X4, X0, X0, #12 0x531c6c05, // LSLW $4 = UBFM W5, W0, #28, #27 0x53087c06, // LSRW $8 = UBFM W6, W0, #8, #31 0x13047c07, // ASRW $4 = SBFM W7, W0, #4, #31 0x13803008, // RORW $12 = EXTR W8, W0, W0, #12 0xd65f03c0, // RET } 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("imm shift word %d = %08x, want %08x", i, got[i], want[i]) } } got = arm64Words(t, "\tLSL R9, R0, R10\n\tLSR R9, R0, R11\n\tASR R9, R0, R12\n\tROR R9, R0, R13\n"+ "\tLSLW R9, R0, R14\n\tLSRW R9, R0, R15\n\tASRW R9, R0, R16\n\tRORW R9, R0, R17\n") want = []uint32{ 0x9ac9200a, // LSLV X10, X0, X9 0x9ac9240b, // LSRV X11, X0, X9 0x9ac9280c, // ASRV X12, X0, X9 0x9ac92c0d, // RORV X13, X0, X9 0x1ac9200e, // LSLV W14, W0, W9 0x1ac9240f, // LSRV W15, W0, W9 0x1ac92810, // ASRV W16, W0, W9 0x1ac92c11, // RORV W17, W0, W9 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("reg shift word %d = %08x, want %08x", i, got[i], want[i]) } } // Two-operand spellings fold to Rn = Rd. got = arm64Words(t, "\tLSL $4, R1\n\tLSR R9, R1\n\tASR $4, R1\n\tROR R9, R1\n\tLSLW $4, R1\n\tRORW R9, R1\n") want = []uint32{ 0xd37cec21, // LSL $4, R1 = UBFM X1, X1, #60, #59 0x9ac92421, // LSRV X1, X1, X9 0x9344fc21, // ASR $4, R1 = SBFM X1, X1, #4, #63 0x9ac92c21, // RORV X1, X1, X9 0x531c6c21, // LSLW $4, R1 = UBFM W1, W1, #28, #27 0x1ac92c21, // RORV W1, W1, W9 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("2op shift word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ShiftRangeErrors: the toolchain reports "illegal bit number" for // shift amounts at or above the operand width. func TestArm64ShiftRangeErrors(t *testing.T) { for _, src := range []string{ "\tLSL $64, R0, R1\n", "\tLSRW $32, R0, R1\n", "\tRORW $32, R0, R1\n", "\tASR $-1, R0, R1\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 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", src) } } } // TestArm64DivEncodings pins SDIV/UDIV in both widths: the 2-source opcode // field (bits 15:10 of the 0xd6<<21 fixed field) is UDIV=0b0010, SDIV=0b0011. func TestArm64DivEncodings(t *testing.T) { got := arm64Words(t, "\tSDIV R1, R2, R3\n\tUDIV R1, R2, R3\n\tSDIVW R1, R2, R3\n\tUDIVW R1, R2, R3\n") want := []uint32{ 0x9ac10c43, // SDIV X3, X2, X1 0x9ac10843, // UDIV X3, X2, X1 0x1ac10c43, // SDIV W3, W2, W1 0x1ac10843, // UDIV W3, W2, W1 0xd65f03c0, // RET } 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("div word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64MAddSub pins the four-operand MADD/MSUB words (Rm, Ra, Rn, Rd, // with Ra in bits 14:10) and rejects the shorter spellings the toolchain // also rejects. func TestArm64MAddSub(t *testing.T) { got := arm64Words(t, "\tMADD R1, R2, R3, R4\n\tMSUB R1, R2, R3, R4\n\tMADDW R1, R2, R3, R5\n\tMSUBW R1, R2, R3, R5\n") want := []uint32{ 0x9b010864, // MADD X4, X3, X1, X2 (Rm=1, Ra=2, Rn=3) 0x9b018864, // MSUB X4, X3, X1, X2 0x1b010865, // MADD W5, W3, W1, W2 0x1b018865, // MSUB W5, W3, W1, W2 0xd65f03c0, // RET } 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("madd word %d = %08x, want %08x", i, got[i], want[i]) } } // The accumulate operand is mandatory: 2- and 3-operand forms error // rather than silently reading R0 or ZR as the accumulator. for _, body := range []string{ "\tMADD R1, R2\n", "\tMADD R1, R2, R3\n", "\tMSUBW R1, R2, R3\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64MovImmWidth pins the immediate classifications whose size pass // once disagreed with the encoder: negative and 0xFFFFFFFF W values go // through MOVN after 32-bit truncation, and 3- to 4-chunk constants expand // to one word per non-zero chunk. func TestArm64MovImmWidth(t *testing.T) { got := arm64Words(t, "\tMOVW $-1, R0\n\tMOVW $0xFFFFFFFF, R3\n") want := []uint32{ 0x12800000, // MOVN W0, #0 0x12800003, // MOVN W3, #0 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("movw word %d = %08x, want %08x", i, got[i], want[i]) } } for _, tt := range []struct { body string words int }{ {"\tMOVD $0x0001000200030000, R2\n", 3}, // three chunks {"\tMOVD $0x0001000200030004, R1\n", 4}, // four chunks {"\tMOVW $-1, R0\n", 1}, // MOVN after truncation } { if got := arm64Words(t, tt.body); len(got) != tt.words+1 { t.Errorf("%s: %d words, want %d (including RET)", tt.body, len(got), tt.words+1) } } } // TestArm64ExclOffsetErrors: exclusive and atomic encodings carry no // immediate field, so a non-zero offset is rejected the way the toolchain // reports "illegal combination" for it, never silently dropped. func TestArm64ExclOffsetErrors(t *testing.T) { for _, body := range []string{ "\tLDXR 8(R1), R2\n", "\tLDAXR 8(R1), R2\n", "\tSTXR R3, 8(R1), R4\n", "\tSTLXR R3, 8(R1), R4\n", "\tCASD R3, 8(R1), R4\n", "\tLDADDD R3, 8(R1), R4\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64ExclNoOffset pins the plain (Rn) forms, byte-for-byte against // go tool asm. The toolchain parses the FIRST register of a store as the // data register and the LAST as the status register (asm7.go case 59), and // the pair forms as (Rt1, Rt2) (case 58/59): // // STXR R3, (R1), R4 → c8047c23 (Rt=3, Rn=1, Rs=4) // STXP (R3, R4), (R1), R5 → c8251023 (Rt=3, Rt2=4, Rn=1, Rs=5) // LDXP (R1), (R3, R4) → c87f1023 (Rn=1, Rt=3, Rt2=4) func TestArm64ExclNoOffset(t *testing.T) { got := arm64Words(t, "\tLDXR (R1), R2\n\tSTXR R3, (R1), R4\n"+ "\tSTXP (R3, R4), (R1), R5\n\tSTXPW (R3, R4), (R1), R5\n"+ "\tLDXP (R1), (R3, R4)\n\tLDXPW (R1), (R3, R4)\n"+ "\tSTXR R3, (RSP), R4\n\tLDXR (RSP), R2\n") want := []uint32{ 0xc85f7c22, // LDXR X2, [X1] 0xc8047c23, // STXR W3, [X1], W4 with Rt = R3, Rs = R4 0xc8251023, // STXP (R3, R4), [X1], R5 0x88251023, // STXPW (R3, R4), [X1], R5 0xc87f1023, // LDXP [X1], (R3, R4) 0x887f1023, // LDXPW [X1], (R3, R4) 0xc8047fe3, // STXR R3, [SP], R4 0xc85f7fe2, // LDXR [SP], R2 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("excl word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64AddSubImmRange: immediates that cannot ride the imm12 field are // rejected instead of wrapping through int32. func TestArm64AddSubImmRange(t *testing.T) { for _, body := range []string{ "\tADD $0x100000000, R0, R1\n", "\tSUB $-0x100000000, R0, R1\n", "\tCMP $0x100000000, R0\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64LargeRegisterOffset pins the large-offset path for a register // base: the ADD offsets from the operand's own base, not from SP, matching // the toolchain's `ADD $(256<<12), R2, R27; MOVD (R27), R3`. func TestArm64LargeRegisterOffset(t *testing.T) { got := arm64Words(t, "\tMOVD 0x100000(R2), R3\n\tMOVD R3, 0x100000(R2)\n") want := []uint32{ 0x9144005b, // ADD $(256<<12), R2, R27 0xf9400363, // MOVD (R27), R3 0x9144005b, // ADD $(256<<12), R2, R27 0xf9000363, // MOVD R3, (R27) 0xd65f03c0, // RET } 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("large offset word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64LargeFrameSpadj checks the stack-adjustment boundaries of a frame // whose autosize must be materialised into REGTMP: $5000 rounds the autosize // to 5024, so the prologue is [MOVD $5024, R27][SUB R27, RSP, R20][STP][ADD // R20, SP][SUB $8] and SP moves only at its fourth word, while the RET's // epilogue is [LDP][MOVD $5024, R27][ADD R27, RSP, RSP] before the final // RET. These PCs feed the DWARF CFA rules and the goobj stack maps. func TestArm64LargeFrameSpadj(t *testing.T) { f, errs := parser.Parse("frame_arm64.s", "#include \"textflag.h\"\n\nTEXT ·framed(SB), $5000-0\n\tCALL ·other(SB)\n\tRET\n\nTEXT ·other(SB), NOSPLIT, $0\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } fn := img.Funcs[0] // autosize 5024: class-2 guard of 6 words (24 bytes), a 5-word prologue // whose ADD R20, SP sits at byte 8 inside it, a one-instruction body, // then a 3-word epilogue before the final RET. wantSpadj := []SpadjStep{{PC: 24 + 12, Value: 5024}, {PC: 24 + 20 + 4 + 12, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } // The words those PCs point between: the prologue's ADD R20, SP at byte // 36, and the epilogue's materialised ADD R27, RSP, RSP right before the // final RET at byte 60. words := leWords(img.Code[fn.Offset : fn.Offset+fn.Size]) if got := words[(24+12)/4]; got != 0x9100029f { t.Errorf("prologue word at byte 36 = %08x, want 9100029f (ADD R20, SP)", got) } if got := words[(24+20+4+8)/4]; got != 0x8b3b63ff { t.Errorf("epilogue word at byte 56 = %08x, want 8b3b63ff (ADD R27, RSP, RSP)", got) } if got := words[(24+20+4+12)/4]; got != 0xd65f03c0 { t.Errorf("final RET word at byte 60 = %08x, want d65f03c0", got) } } // TestArm64SplitFrameSpadj pins the addcon2 band, where neither imm12 form // nor a single MOVZ carries the autosize and the toolchain splits the // prologue SUB into two imm12 instructions (asm7.go case 48) while the // non-leaf RET still materialises the value into REGTMP (obj7.go ARET, // issue 73259). $65664 rounds the autosize to 65680 = 144 + 16<<12: // // [SUB $144, RSP, R20][SUB $(16<<12), R20, R20][STP][MOVD R20, SP][SUB $8] // [CALL] // [LDP][MOVD $144, R27][MOVK $(1<<16), R27][ADD R27, RSP, RSP][RET] // // SP moves at the fourth word (byte 12) and returns to zero at the final // RET (byte 40); the words are go tool asm's own for the same source. func TestArm64SplitFrameSpadj(t *testing.T) { f, errs := parser.Parse("frame_arm64.s", "#include \"textflag.h\"\n\nTEXT ·framed(SB), NOSPLIT, $65664-0\n\tCALL ·other(SB)\n\tRET\n\nTEXT ·other(SB), NOSPLIT, $0\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } fn := img.Funcs[0] wantSpadj := []SpadjStep{{PC: 12, Value: 65680}, {PC: 40, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } want := []uint32{ 0xd10243f4, // SUB $144, RSP, R20 0xd1404294, // SUB $(16<<12), R20, R20 0xa93ffa9d, // STP (R29, R30), -8(R20) 0x9100029f, // MOVD R20, RSP 0xd10023fd, // SUB $8, RSP, R29 0x94000000, // CALL (relocation masked at link time) 0xa97ffbfd, // LDP -8(RSP), (R29, R30) 0xd280121b, // MOVD $144, R27 0xf2a0003b, // MOVK $(1<<16), R27 0x8b3b63ff, // ADD R27, RSP, RSP 0xd65f03c0, // RET } words := leWords(img.Code[fn.Offset : fn.Offset+fn.Size]) if len(words) != len(want) { t.Fatalf("framed = %d words, want %d", len(words), len(want)) } for i, w := range want { if words[i] != w { t.Errorf("word %d = %08x, want %08x", i, words[i], w) } } }