327 lines
11 KiB
Go
327 lines
11 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package asm
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||
)
|
||
|
||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||
// the symbol tables, the function code bytes and the relocation wiring.
|
||
func TestGOObjectLOONG64Structure(t *testing.T) {
|
||
f, errs := parser.Parse("k_loong64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·add(SB), NOSPLIT, $0-24
|
||
MOVV a+0(FP), R4
|
||
MOVV b+8(FP), R5
|
||
ADDV R5, R4, R4
|
||
MOVV R4, ret+16(FP)
|
||
RET
|
||
|
||
GLOBL ·table<>(SB), RODATA, $8
|
||
DATA ·table<>+0(SB)/8, $0x1122334455667788
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFileLOONG64(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||
}
|
||
obj, err := img.GOObjectLOONG64("testpkg", "k_loong64.s")
|
||
if err != nil {
|
||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||
}
|
||
v := openGoobj(t, obj)
|
||
|
||
// Package defs: the static GLOBL, then the FuncInfo and the two DWARF
|
||
// symbols (debug_line program, subprogram DIE).
|
||
defs := v.syms(blkSymdef)
|
||
if len(defs) != 4 {
|
||
t.Fatalf("symdefs = %d, want 4", len(defs))
|
||
}
|
||
if defs[0].name != "table" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 8 {
|
||
t.Errorf("table symbol = %+v", defs[0])
|
||
}
|
||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||
t.Errorf("funcinfo symbol = %+v", defs[1])
|
||
}
|
||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||
t.Errorf("lines symbol = %+v", defs[2])
|
||
}
|
||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||
t.Errorf("DIE symbol = %+v", defs[3])
|
||
}
|
||
|
||
// Non-package defs: four pc tables and the function.
|
||
nps := v.syms(blkNonpkgdef)
|
||
if len(nps) != 5 {
|
||
t.Fatalf("nonpkgdefs = %d, want 5", len(nps))
|
||
}
|
||
fn := nps[4]
|
||
if fn.name != "testpkg.add" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 20 {
|
||
t.Errorf("add symbol = %+v", fn)
|
||
}
|
||
|
||
// The function code: 20 bytes, the ground-truth encoding. It sits
|
||
// after the GLOBL, FuncInfo, two DWARF symbols and four pc tables.
|
||
dataIdx := v.blk(blkDataIdx)
|
||
dataBlk := v.blk(blkData)
|
||
le := binary.LittleEndian
|
||
dOff := le.Uint32(dataIdx[8*4:])
|
||
code := dataBlk[dOff : dOff+20]
|
||
want := []byte{
|
||
0x64, 0x20, 0xc0, 0x28, // ld.d r4, 8(r3)
|
||
0x65, 0x40, 0xc0, 0x28, // ld.d r5, 16(r3)
|
||
0x84, 0x94, 0x10, 0x00, // add.d r4, r4, r5
|
||
0x64, 0x60, 0xc0, 0x29, // st.d r4, 24(r3)
|
||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||
}
|
||
for i := range want {
|
||
if code[i] != want[i] {
|
||
t.Fatalf("code byte %d = %02x, want %02x", i, code[i], want[i])
|
||
}
|
||
}
|
||
|
||
// The debug_line program: LNE_set_address (the R_ADDR relocation
|
||
// carries the function address), then one row per line change — the
|
||
// TEXT is on line 4 (a leading blank line precedes the include), the
|
||
// instructions on lines 5–9 — an advance to the 20-byte end and an
|
||
// end-of-sequence.
|
||
linesOff := le.Uint32(dataIdx[4*2:])
|
||
lines := dataBlk[linesOff : linesOff+21]
|
||
wantLines := []byte{
|
||
0x00, 0x09, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LNE_set_address
|
||
0x13, // pc 0, line 5
|
||
0x38, // pc 4, line 6
|
||
0x38, // pc 8, line 7
|
||
0x38, // pc 12, line 8
|
||
0x38, // pc 16, line 9
|
||
0x02, 0x04, // advance_pc to 20
|
||
0x00, 0x01, 0x01, // end_sequence
|
||
}
|
||
for i := range wantLines {
|
||
if lines[i] != wantLines[i] {
|
||
t.Fatalf("lines byte %d = %02x, want %02x", i, lines[i], wantLines[i])
|
||
}
|
||
}
|
||
|
||
// The subprogram DIE: abbrev 3 (FUNCTION), the qualified name, the
|
||
// addrx low_pc slot (R_DWTXTADDR_U4), the size as high_pc, the
|
||
// call-frame-CFA frame base, decl file/line and the external flag.
|
||
dieOff := le.Uint32(dataIdx[4*3:])
|
||
die := dataBlk[dieOff : dieOff+27]
|
||
wantDie := []byte{
|
||
0x03,
|
||
't', 'e', 's', 't', 'p', 'k', 'g', '.', 'a', 'd', 'd', 0,
|
||
0x00, 0x00, 0x00, 0x00, // low_pc: addrx slot
|
||
0x14, // high_pc: 20
|
||
0x01, 0x9c, // frame_base: DW_OP_call_frame_cfa
|
||
0x01, 0x00, 0x00, 0x00, // decl_file: 1
|
||
0x04, // decl_line: 4
|
||
0x01, // external
|
||
0x00, // end of children
|
||
}
|
||
for i := range wantDie {
|
||
if die[i] != wantDie[i] {
|
||
t.Fatalf("DIE byte %d = %02x, want %02x", i, die[i], wantDie[i])
|
||
}
|
||
}
|
||
|
||
// The DWARF symbols carry the function-address references: R_ADDR for
|
||
// the line program's set_address, R_DWTXTADDR_U4 for the DIE's addrx
|
||
// slot, both against the function's non-package index. The reloc
|
||
// index counts relocations, not bytes.
|
||
relocIdx := v.blk(blkRelocIdx)
|
||
relocs := v.blk(blkReloc)
|
||
if le.Uint32(relocIdx[4*2:]) != 0 || le.Uint32(relocIdx[4*3:]) != 1 || le.Uint32(relocIdx[4*4:]) != 2 {
|
||
t.Fatalf("dwarf reloc index ranges: %d %d %d", le.Uint32(relocIdx[4*2:]), le.Uint32(relocIdx[4*3:]), le.Uint32(relocIdx[4*4:]))
|
||
}
|
||
lr := relocs[:23]
|
||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||
t.Errorf("lines reloc = %x", lr)
|
||
}
|
||
dr := relocs[23:46]
|
||
if int32(le.Uint32(dr[0:])) != 13 || dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4 ||
|
||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||
t.Errorf("die reloc = %x", dr)
|
||
}
|
||
|
||
// The pc-value deltas are in MinLC (4) units: the flat pcsp covers
|
||
// the whole 20-byte function with a delta of 5.
|
||
pcspOff := le.Uint32(dataIdx[4*4:])
|
||
if got := dataBlk[pcspOff : pcspOff+3]; !bytes.Equal(got, []byte{0x02, 0x05, 0x00}) {
|
||
t.Errorf("pcsp = %x, want 020500", got)
|
||
}
|
||
}
|
||
|
||
// TestGOObjectLOONG64Link cross-compiles a Go program with the gasm-produced
|
||
// object substituted into the package archive, proving cmd/link accepts the
|
||
// emitted GOOBJ. The binary is not executed (no LoongArch host or qemu).
|
||
// Skipped when no Go toolchain is available.
|
||
func TestGOObjectLOONG64Link(t *testing.T) {
|
||
goBin, err := exec.LookPath("go")
|
||
if err != nil {
|
||
t.Skip("no Go toolchain available")
|
||
}
|
||
dir := t.TempDir()
|
||
asmSrc := `#include "textflag.h"
|
||
TEXT ·add(SB), NOSPLIT, $0-24
|
||
MOVV a+0(FP), R4
|
||
MOVV b+8(FP), R5
|
||
ADDV R5, R4, R4
|
||
MOVV R4, ret+16(FP)
|
||
RET
|
||
`
|
||
if err := os.WriteFile(filepath.Join(dir, "main_loong64.s"), []byte(asmSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
mainSrc := `package main
|
||
|
||
func add(a, b int64) int64
|
||
|
||
func main() {
|
||
if add(20, 22) != 42 {
|
||
panic("bad add")
|
||
}
|
||
}
|
||
`
|
||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module l64link\n\ngo 1.21\n"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Capture the cross build (GOARCH=loong64): the package archive and the
|
||
// link line.
|
||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||
build.Dir = dir
|
||
build.Env = append(os.Environ(), "GOARCH=loong64")
|
||
buildLog, err := build.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||
}
|
||
var pkgArch, work, linkLine, asmObj string
|
||
for _, line := range strings.Split(string(buildLog), "\n") {
|
||
switch {
|
||
case strings.HasPrefix(line, "WORK="):
|
||
work = strings.TrimPrefix(line, "WORK=")
|
||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_loong64.s") && !strings.Contains(line, "-gensymabis"):
|
||
asmObj = fieldAfter(line, "-o")
|
||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||
linkLine = line
|
||
}
|
||
}
|
||
if pkgArch == "" || linkLine == "" || asmObj == "" {
|
||
t.Skip("could not locate the archive, asm output or link line in the build log")
|
||
}
|
||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||
// The archive member holding the assembler's output is named after the
|
||
// asm object file (main_loong64.o), as cmd/go packs it with `pack r`.
|
||
asmMember := filepath.Base(strings.ReplaceAll(asmObj, "$WORK", work))
|
||
|
||
// Assemble the same source with gasm and swap the object in.
|
||
pf, perrs := parser.Parse(filepath.Join(dir, "main_loong64.s"), asmSrc)
|
||
if len(perrs) > 0 {
|
||
t.Fatalf("parse: %v", perrs)
|
||
}
|
||
pimg, err := AssembleFileLOONG64(pf)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||
}
|
||
obj, err := pimg.GOObjectLOONG64("main", filepath.Join(dir, "main_loong64.s"))
|
||
if err != nil {
|
||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||
}
|
||
|
||
// Extract the archive, substitute the object member, repack.
|
||
membersDir := filepath.Join(dir, "members")
|
||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||
extract.Dir = membersDir
|
||
extract.Env = append(os.Environ(), "GOARCH=loong64")
|
||
if out, err := extract.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack x: %v\n%s", err, out)
|
||
}
|
||
// Substitute the gasm object for the assembler's archive member (pack
|
||
// extracts members read-only).
|
||
member := filepath.Join(membersDir, asmMember)
|
||
if err := os.Chmod(member, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||
listCmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||
listOut, err := listCmd.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||
}
|
||
newArch := filepath.Join(dir, "pkg.a")
|
||
args := []string{"tool", "pack", "c", newArch}
|
||
seen := map[string]bool{}
|
||
for _, m := range strings.Fields(string(listOut)) {
|
||
if seen[m] {
|
||
continue
|
||
}
|
||
seen[m] = true
|
||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
args = append(args, filepath.Join(membersDir, m))
|
||
}
|
||
pack := exec.Command(goBin, args...)
|
||
pack.Dir = membersDir
|
||
pack.Env = append(os.Environ(), "GOARCH=loong64")
|
||
if out, err := pack.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack c: %v\n%s", err, out)
|
||
}
|
||
|
||
// Re-link with our archive in place of the toolchain's. The link line
|
||
// carries a GOROOT assignment and $WORK placeholders; run it through the
|
||
// shell with the GOEXPERIMENT and GOARCH the toolchain expects (the
|
||
// linker compares the object header against its own, experiments
|
||
// included).
|
||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||
link := exec.Command("sh", "-c", linkLine)
|
||
link.Dir = dir
|
||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)), "GOARCH=loong64")
|
||
if out, err := link.CombinedOutput(); err != nil {
|
||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||
}
|
||
|
||
// The binary is not executed: there is no LoongArch host or qemu here.
|
||
// The link itself and the symbol table prove cmd/link accepted the gasm
|
||
// object and laid out the function.
|
||
nm := exec.Command(goBin, "tool", "nm", filepath.Join(dir, "app2"))
|
||
nm.Env = append(os.Environ(), "GOARCH=loong64")
|
||
nmOut, err := nm.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("nm gasm-linked binary: %v\n%s", err, nmOut)
|
||
}
|
||
if !strings.Contains(string(nmOut), "main.add") {
|
||
t.Errorf("main.add not found in linked binary:\n%s", nmOut)
|
||
}
|
||
}
|