feat: gasm-devkit 0.1.0 — GAsm lexer, parser, linter, formatter, LSP and amd64 assembler
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// firstText parses src and returns its first TEXT function.
|
||||
func firstText(t *testing.T, src string) *ast.Text {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("f_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
if txt, ok := d.(*ast.Text); ok {
|
||||
return txt
|
||||
}
|
||||
}
|
||||
t.Fatal("no TEXT function found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// disasm decodes a machine-code blob into Intel-syntax instruction strings.
|
||||
func disasm(t *testing.T, code []byte) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
for len(code) > 0 {
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("decode %x: %v", code, err)
|
||||
}
|
||||
out = append(out, x86asm.IntelSyntax(inst, 0, nil))
|
||||
code = code[inst.Len:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hexBytes(b []byte) string {
|
||||
var sb strings.Builder
|
||||
for _, x := range b {
|
||||
sb.WriteString(" ")
|
||||
const hexdig = "0123456789abcdef"
|
||||
sb.WriteByte(hexdig[x>>4])
|
||||
sb.WriteByte(hexdig[x&0xf])
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
func TestAssembleLoop(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
XORQ AX, AX
|
||||
loop:
|
||||
ADDQ $1, AX
|
||||
CMPQ $10, AX
|
||||
JLT loop
|
||||
RET
|
||||
`)
|
||||
code, labels, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
if _, ok := labels["loop"]; !ok {
|
||||
t.Fatalf("label 'loop' not recorded: %v", labels)
|
||||
}
|
||||
got := strings.Join(disasm(t, code), "\n")
|
||||
want := strings.Join([]string{
|
||||
"xor rax, rax",
|
||||
"add rax, 0x1",
|
||||
"cmp rax, 0xa",
|
||||
"jl 0x0",
|
||||
"ret",
|
||||
}, "\n")
|
||||
gotLines := strings.Split(got, "\n")
|
||||
wantLines := strings.Split(want, "\n")
|
||||
if len(gotLines) != len(wantLines) {
|
||||
t.Fatalf("instruction count mismatch:\n got:\n%s\n want:\n%s", got, want)
|
||||
}
|
||||
for i := range wantLines {
|
||||
if strings.HasPrefix(wantLines[i], "jl") {
|
||||
if !strings.HasPrefix(gotLines[i], "jl") {
|
||||
t.Errorf("line %d: got %q, want a jl", i, gotLines[i])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if gotLines[i] != wantLines[i] {
|
||||
t.Errorf("line %d: got %q, want %q", i, gotLines[i], wantLines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembleMemory(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·g(SB), NOSPLIT, $0
|
||||
MOVQ (AX), BX
|
||||
MOVQ 8(AX), CX
|
||||
LEAQ (AX)(BX*4), DX
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
got := strings.Join(disasm(t, code), "\n")
|
||||
want := strings.Join([]string{
|
||||
"mov rbx, qword ptr [rax]",
|
||||
"mov rcx, qword ptr [rax+0x8]",
|
||||
"lea rdx, ptr [rax+4*rbx]",
|
||||
"ret",
|
||||
}, "\n")
|
||||
if got != want {
|
||||
t.Errorf("assemble memory:\n got:\n%s\n want:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFP verifies the FP pseudo-register translation for a NOSPLIT $0
|
||||
// function against the exact bytes the Go assembler produces (verified via
|
||||
// `go tool objdump`): x+N(FP) maps to (N+8)(SP).
|
||||
func TestAssembleFP(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·loadarg(SB), NOSPLIT, $0-24
|
||||
MOVQ p+0(FP), AX
|
||||
MOVQ n+8(FP), CX
|
||||
ADDQ CX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
// From `go tool objdump` of the Go-assembled function:
|
||||
// MOVQ 0x8(SP), AX 488b442408
|
||||
// MOVQ 0x10(SP), CX 488b4c2410
|
||||
// ADDQ CX, AX 4801c8
|
||||
// MOVQ AX, 0x18(SP) 4889442418
|
||||
// RET c3
|
||||
want := []byte{
|
||||
0x48, 0x8b, 0x44, 0x24, 0x08,
|
||||
0x48, 0x8b, 0x4c, 0x24, 0x10,
|
||||
0x48, 0x01, 0xc8,
|
||||
0x48, 0x89, 0x44, 0x24, 0x18,
|
||||
0xc3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("FP translation mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFrame verifies a function with a non-zero frame: the Go-style
|
||||
// prologue/epilogue and the x+N(FP) → (N+frame+16)(SP) translation, against
|
||||
// the bytes the Go assembler produces.
|
||||
func TestAssembleFrame(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·withframe(SB), NOSPLIT, $16-16
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), CX
|
||||
ADDQ CX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
// From `go tool objdump`:
|
||||
// PUSHQ BP 55
|
||||
// MOVQ SP, BP 4889e5
|
||||
// SUBQ $0x10, SP 4883ec10
|
||||
// MOVQ 0x20(SP), AX 488b442420 (0 + 16 + 16)
|
||||
// MOVQ 0x28(SP), CX 488b4c2428 (8 + 16 + 16)
|
||||
// ADDQ CX, AX 4801c8
|
||||
// MOVQ AX, 0x30(SP) 4889442430 (16 + 16 + 16)
|
||||
// ADDQ $0x10, SP 4883c410
|
||||
// POPQ BP 5d
|
||||
// RET c3
|
||||
want := []byte{
|
||||
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x10,
|
||||
0x48, 0x8b, 0x44, 0x24, 0x20,
|
||||
0x48, 0x8b, 0x4c, 0x24, 0x28,
|
||||
0x48, 0x01, 0xc8,
|
||||
0x48, 0x89, 0x44, 0x24, 0x30,
|
||||
0x48, 0x83, 0xc4, 0x10, 0x5d, 0xc3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("frame translation mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user