2026-08-20 13:32:52 +02:00
|
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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))
|
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
|
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #48; RET
|
|
|
|
|
|
expected := []uint32{0xf85f83fd, 0xf84307fe, 0xd65f03c0}
|
2026-08-20 13:32:52 +02:00
|
|
|
|
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
|
2026-08-20 14:31:15 +02:00
|
|
|
|
{2, 1, 1, 63, 0, true}, // single bit at pos 1 (immr = esize-1)
|
2026-08-20 13:32:52 +02:00
|
|
|
|
{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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-20 15:44:23 +02:00
|
|
|
|
// 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")
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-09-19 19:17:07 +02:00
|
|
|
|
|
|
|
|
|
|
// 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])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|