feat(asm): emit RISC-V GOOBJ with the shared emitter
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// TestGOObjectRISCVStructure checks the emitted RISC-V object's relocations:
|
||||
// each AUIPC + second-instruction pair carries a single 8-byte
|
||||
// R_RISCV_PCREL_ITYPE/STYPE relocation, exactly as `go tool asm` models it
|
||||
// (rather than the ELF HI20/LO12 pair).
|
||||
func TestGOObjectRISCVStructure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·sb(SB), NOSPLIT, $0-0
|
||||
MOV $answer<>(SB), X10
|
||||
MOV answer<>(SB), X11
|
||||
MOV X12, answer<>(SB)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 26 {
|
||||
t.Fatalf("function size = %d, want 26", fn.Size)
|
||||
}
|
||||
if len(fn.Relocs) != 3 {
|
||||
t.Fatalf("relocs = %d, want 3", len(fn.Relocs))
|
||||
}
|
||||
wantKind := []RelocKind{RelRISCVPCRELIType, RelRISCVPCRELIType, RelRISCVPCRELSType}
|
||||
wantOff := []int{0, 8, 16}
|
||||
for i, r := range fn.Relocs {
|
||||
if r.Kind != wantKind[i] || r.Off != wantOff[i] || r.After != r.Off+8 || r.Name != "answer" || r.Addend != 0 {
|
||||
t.Errorf("reloc %d = {kind %v off %d after %d name %q addend %d}", i, r.Kind, r.Off, r.After, r.Name, r.Addend)
|
||||
}
|
||||
}
|
||||
|
||||
obj, err := img.GOObjectRISCV("testpkg", "k_riscv64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectRISCV: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
|
||||
// Package defs: the static GLOBL, the FuncInfo, then the two DWARF
|
||||
// symbols.
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 4 {
|
||||
t.Fatalf("symdefs = %d, want 4", len(defs))
|
||||
}
|
||||
if defs[0].name != "answer" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 8 {
|
||||
t.Errorf("answer symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[2].typ != kindSDWARFLINES || defs[3].typ != kindSDWARFFCN {
|
||||
t.Errorf("dwarf symbols = %+v, %+v", defs[2], defs[3])
|
||||
}
|
||||
|
||||
// The three code relocations, in definition order: ITYPE, ITYPE, STYPE,
|
||||
// each 8 bytes wide against the GLOBL (package symbol 0).
|
||||
relocIdx := v.blk(blkRelocIdx)
|
||||
relocs := v.blk(blkReloc)
|
||||
if len(relocs) != 5*23 {
|
||||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||||
}
|
||||
// The function is the last non-package symbol; its relocs start after
|
||||
// the DWARF symbols' (defs 2 and 3 each carry one).
|
||||
le := binary.LittleEndian
|
||||
first := int(le.Uint32(relocIdx[4*(4+4):]))
|
||||
wantType := []uint16{relocRISCVPcrelItype, relocRISCVPcrelItype, relocRISCVPcrelStype}
|
||||
wantOffAbs := []int{0, 8, 16}
|
||||
for i := 0; i < 3; i++ {
|
||||
e := relocs[(first+i)*23:]
|
||||
if int32(le.Uint32(e[0:])) != int32(wantOffAbs[i]) || e[4] != 8 || le.Uint16(e[5:]) != wantType[i] ||
|
||||
le.Uint32(e[15:]) != pkgIdxSelf || le.Uint32(e[19:]) != 0 {
|
||||
t.Errorf("reloc %d = off %d size %d type %d pkg %d sym %d", i, int32(le.Uint32(e[0:])), e[4], le.Uint16(e[5:]), le.Uint32(e[15:]), le.Uint32(e[19:]))
|
||||
}
|
||||
}
|
||||
|
||||
// The function code: three AUIPC+second-instruction pairs with zero
|
||||
// immediates, then the compressed return.
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
want := append(wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 10, 0)), wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 10, 10, 0))...)
|
||||
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 11, 0))...)
|
||||
want = append(want, wordLE(riscvIType(riscvEnc{0x03, 0x3, 0x00}, 11, 11, 0))...)
|
||||
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 31, 0))...)
|
||||
want = append(want, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 31, 12, 0))...)
|
||||
want = append(want, 0x82, 0x80) // C.JR ra
|
||||
if !bytes.Equal(code, want) {
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
|
||||
// The same bytes must survive into the object's data block intact: the
|
||||
// linker patches only the immediate fields of the AUIPC pairs, so the
|
||||
// opcode/register bits of every instruction must not be zeroed.
|
||||
dataIdx := v.blk(blkDataIdx)
|
||||
dataBlk := v.blk(blkData)
|
||||
dOff := int(le.Uint32(dataIdx[8*4:])) // the function is the last symbol
|
||||
emitted := dataBlk[dOff : dOff+fn.Size]
|
||||
if !bytes.Equal(emitted, want) {
|
||||
t.Errorf("emitted data = % x\nwant % x", emitted, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectRISCVLink cross-compiles a Go program with the gasm-produced
|
||||
// object substituted into the package archive, proving cmd/link accepts the
|
||||
// emitted RISC-V GOOBJ. The binary is not executed (no riscv64 host or
|
||||
// qemu). Skipped when no Go toolchain is available.
|
||||
func TestGOObjectRISCVLink(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
|
||||
MOV a+0(FP), X10
|
||||
MOV b+8(FP), X11
|
||||
ADD X11, X10, X10
|
||||
MOV X10, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_riscv64.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 rvlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
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_riscv64.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)
|
||||
asmMember := filepath.Base(strings.ReplaceAll(asmObj, "$WORK", work))
|
||||
|
||||
pf, perrs := parser.Parse(filepath.Join(dir, "main_riscv64.s"), asmSrc)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs)
|
||||
}
|
||||
pimg, err := AssembleFileRISCV(pf)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
obj, err := pimg.GOObjectRISCV("main", filepath.Join(dir, "main_riscv64.s"))
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectRISCV: %v", err)
|
||||
}
|
||||
|
||||
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=riscv64")
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
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=riscv64")
|
||||
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=riscv64")
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
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=riscv64")
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
nm := exec.Command(goBin, "tool", "nm", filepath.Join(dir, "app2"))
|
||||
nm.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user