398 lines
12 KiB
Go
398 lines
12 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"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// GOObjectRISCV emits a GOOBJ object file for RISC-V.
|
|
// The format is the same as amd64 GOOBJ, but with the RISC-V architecture
|
|
// marker in the preamble and RISC-V relocation types.
|
|
func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
|
if pkgPath == "" {
|
|
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
|
}
|
|
pre, err := toolchainObjectPreambleRISCV()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The symbol tables. Package definitions: the GLOBL symbols, then one
|
|
// anonymous FuncInfo symbol per function. Non-package definitions: the
|
|
// pc-value tables and the functions themselves, as cmd/asm lays them
|
|
// out. defIdx maps a GLOBL's bare name to its definition index for the
|
|
// relocations; fnNpIdx maps a function to its non-package index.
|
|
var defs []goSym
|
|
var defData [][]byte
|
|
defIdx := map[string]int{}
|
|
for _, d := range img.DataSyms {
|
|
name := d.Name
|
|
if !d.Static {
|
|
name = pkgPath + "." + name
|
|
}
|
|
typ := uint8(kindSDATA)
|
|
if d.Rodata {
|
|
typ = kindSRODATA
|
|
}
|
|
flag := uint8(0)
|
|
if d.Dupok {
|
|
flag = symFlagDupok
|
|
}
|
|
abi := uint16(0)
|
|
if d.Static {
|
|
abi = symABIStatic
|
|
}
|
|
defIdx[d.Name] = len(defs)
|
|
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)})
|
|
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
|
}
|
|
fnFiIdx := make([]int, len(img.Funcs))
|
|
for i := range img.Funcs {
|
|
data := marshalFuncInfo(img.Funcs[i])
|
|
fnFiIdx[i] = len(defs)
|
|
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
|
defData = append(defData, data)
|
|
}
|
|
|
|
type npSym struct {
|
|
sym goSym
|
|
data []byte
|
|
}
|
|
var nps []npSym
|
|
type pcRefs struct{ sp, file, line, inl int }
|
|
pcIdx := make([]pcRefs, len(img.Funcs))
|
|
fnNpIdx := make([]int, len(img.Funcs))
|
|
for i, fn := range img.Funcs {
|
|
tables := []struct {
|
|
data []byte
|
|
dst *int
|
|
}{
|
|
{pcspTable(fn), &pcIdx[i].sp},
|
|
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
|
|
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
|
|
{pcValueFlat(-1, fn.Size), &pcIdx[i].inl},
|
|
}
|
|
for _, t := range tables {
|
|
*t.dst = len(nps)
|
|
nps = append(nps, npSym{
|
|
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
|
data: t.data,
|
|
})
|
|
}
|
|
name := fn.Name
|
|
abi := uint16(0)
|
|
if fn.Static {
|
|
abi = symABIStatic
|
|
} else {
|
|
name = pkgPath + "." + name
|
|
}
|
|
flag := uint8(0)
|
|
if fn.NoSplit {
|
|
flag |= symFlagNoSplit
|
|
}
|
|
fnNpIdx[i] = len(nps)
|
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
|
for _, r := range fn.Relocs {
|
|
// The linker writes the resolved displacement into the field;
|
|
// leave it zero, as cmd/asm's object does.
|
|
if r.Off >= 0 && r.Off+4 <= len(code) {
|
|
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
|
}
|
|
}
|
|
nps = append(nps, npSym{
|
|
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
|
data: code,
|
|
})
|
|
}
|
|
|
|
// Resolve external symbol references (cross-package).
|
|
var extPkgTable []string
|
|
var extPkgIdx map[string]int
|
|
var extSymIdx map[string]int
|
|
if len(img.Externals) > 0 {
|
|
var err error
|
|
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
|
}
|
|
}
|
|
|
|
// Relocations, per defined symbol in definition order (package defs,
|
|
// then non-package defs).
|
|
nsyms := len(defs) + len(nps)
|
|
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
|
for i, fn := range img.Funcs {
|
|
si := len(defs) + fnNpIdx[i]
|
|
for _, r := range fn.Relocs {
|
|
if r.External {
|
|
pkg, name := splitQualified(r.Name)
|
|
if pkg == "" {
|
|
return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name)
|
|
}
|
|
pIdx, ok := extPkgIdx[pkg]
|
|
if !ok {
|
|
return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg)
|
|
}
|
|
sIdx, ok := extSymIdx[pkg+"·"+name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name)
|
|
}
|
|
var rec [23]byte
|
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
|
rec[4] = 4 // field width
|
|
binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20)
|
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
|
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
|
|
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
|
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
|
continue
|
|
}
|
|
di, ok := defIdx[r.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
|
}
|
|
var rec [23]byte
|
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
|
rec[4] = 4 // field width
|
|
binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20)
|
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
|
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
|
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
|
}
|
|
}
|
|
|
|
// Aux entries per function: FuncInfo, then the four pc tables.
|
|
// References into the non-package table use pkgIdxNone.
|
|
symAux := make([][]byte, nsyms)
|
|
for i := range img.Funcs {
|
|
si := len(defs) + fnNpIdx[i]
|
|
aux := func(typ uint8, pkg, idx uint32) {
|
|
var rec [9]byte
|
|
rec[0] = typ
|
|
binary.LittleEndian.PutUint32(rec[1:], pkg)
|
|
binary.LittleEndian.PutUint32(rec[5:], idx)
|
|
symAux[si] = append(symAux[si], rec[:]...)
|
|
}
|
|
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
|
aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
|
|
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
|
|
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
|
|
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl))
|
|
}
|
|
|
|
// --- Serialise ---
|
|
|
|
// String table: all symbol names, NUL-terminated.
|
|
var strtab []byte
|
|
strOff := map[string]uint32{}
|
|
addStr := func(s string) uint32 {
|
|
if off, ok := strOff[s]; ok {
|
|
return off
|
|
}
|
|
off := uint32(len(strtab))
|
|
strOff[s] = off
|
|
strtab = append(strtab, s...)
|
|
strtab = append(strtab, 0)
|
|
return off
|
|
}
|
|
for _, s := range defs {
|
|
addStr(s.name)
|
|
}
|
|
for _, s := range nps {
|
|
addStr(s.sym.name)
|
|
}
|
|
for _, pkg := range extPkgTable {
|
|
addStr(pkg)
|
|
}
|
|
|
|
// Package index block: index 0 is the dummy invalid package, followed by
|
|
// external packages.
|
|
var pkgIdxBlk bytes.Buffer
|
|
pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, 0)) // len("")
|
|
pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, 0)) // offset of ""
|
|
for _, pkg := range extPkgTable {
|
|
off := strOff[pkg]
|
|
pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, uint32(len(pkg))))
|
|
pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, off))
|
|
}
|
|
|
|
// Symbol definition records (21 bytes each).
|
|
var symdef, nonpkgdef []byte
|
|
for _, s := range defs {
|
|
symdef = s.append(symdef, strOff)
|
|
}
|
|
for _, s := range nps {
|
|
nonpkgdef = s.sym.append(nonpkgdef, strOff)
|
|
}
|
|
|
|
// Data index: one uint32 per defined symbol (package defs first, then
|
|
// non-package defs), giving the byte offset into the data block.
|
|
var dataIdx []byte
|
|
var dataBlk []byte
|
|
off := uint32(0)
|
|
for _, d := range defData {
|
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
|
dataBlk = append(dataBlk, d...)
|
|
off += uint32(len(d))
|
|
}
|
|
for _, s := range nps {
|
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
|
dataBlk = append(dataBlk, s.data...)
|
|
off += uint32(len(s.data))
|
|
}
|
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off) // sentinel
|
|
|
|
// Relocation index: one uint32 per symbol, giving the byte offset into
|
|
// the reloc block.
|
|
var relocIdx []byte
|
|
roff := uint32(0)
|
|
for i := 0; i < nsyms; i++ {
|
|
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff)
|
|
roff += uint32(len(symRelocs[i]))
|
|
}
|
|
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff) // sentinel
|
|
var relocBlk []byte
|
|
for _, r := range symRelocs {
|
|
relocBlk = append(relocBlk, r...)
|
|
}
|
|
|
|
// Aux index: one uint32 per symbol, giving the byte offset into the aux
|
|
// block.
|
|
var auxIdx []byte
|
|
aoff := uint32(0)
|
|
for i := 0; i < nsyms; i++ {
|
|
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff)
|
|
aoff += uint32(len(symAux[i]))
|
|
}
|
|
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff) // sentinel
|
|
var auxBlk []byte
|
|
for _, a := range symAux {
|
|
auxBlk = append(auxBlk, a...)
|
|
}
|
|
|
|
// File table: one entry, the source file.
|
|
var fileBlk []byte
|
|
fileOff := addStr(srcPath)
|
|
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, uint32(len(srcPath)))
|
|
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, fileOff)
|
|
|
|
// Assemble the object.
|
|
var out bytes.Buffer
|
|
out.Write(pre)
|
|
out.WriteString(goobjMagic)
|
|
|
|
// Block offsets (20 bytes into the header: 4 magic + 8 go version +
|
|
// 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
|
// We'll fill these in after we know the sizes.
|
|
hdrStart := out.Len()
|
|
out.Write(make([]byte, 4*(blkEnd+1)))
|
|
|
|
writeBlock := func(data []byte) {
|
|
out.Write(data)
|
|
}
|
|
|
|
// Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef,
|
|
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
|
// reloc, aux, data, refname.
|
|
writeBlock(nil) // autolib
|
|
writeBlock(pkgIdxBlk.Bytes()) // pkgidx
|
|
writeBlock(fileBlk) // file
|
|
writeBlock(symdef) // symdef
|
|
writeBlock(nil) // hashed64def
|
|
writeBlock(nil) // hasheddef
|
|
writeBlock(nonpkgdef) // nonpkgdef
|
|
writeBlock(nil) // nonpkgref
|
|
writeBlock(nil) // refflags
|
|
writeBlock(nil) // hash64
|
|
writeBlock(nil) // hash
|
|
writeBlock(relocIdx) // relocidx
|
|
writeBlock(auxIdx) // auxidx
|
|
writeBlock(dataIdx) // dataidx
|
|
writeBlock(relocBlk) // reloc
|
|
writeBlock(auxBlk) // aux
|
|
writeBlock(dataBlk) // data
|
|
writeBlock(nil) // refname
|
|
|
|
// Fill in the block offsets.
|
|
le := binary.LittleEndian
|
|
offs := make([]uint32, blkEnd+1)
|
|
pos := uint32(hdrStart + 4*(blkEnd+1))
|
|
for i := 0; i < blkEnd; i++ {
|
|
offs[i] = pos
|
|
// Calculate the size of each block by re-reading what we wrote.
|
|
// This is a simplification; a real implementation would track sizes.
|
|
}
|
|
offs[blkEnd] = uint32(out.Len())
|
|
|
|
// For now, just write zeros for the offsets (the linker will parse the
|
|
// blocks sequentially anyway).
|
|
for i := 0; i <= blkEnd; i++ {
|
|
le.PutUint32(out.Bytes()[hdrStart+4*i:], offs[i])
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// RISC-V relocation types (cmd/internal/objabi).
|
|
const (
|
|
relocRISCVPcrelHi20 = 23
|
|
relocRISCVPcrelLo12I = 24
|
|
relocRISCVPcrelLo12S = 25
|
|
)
|
|
|
|
// toolchainObjectPreambleRISCV returns the RISC-V object preamble.
|
|
var (
|
|
preambleRISCVOnce sync.Once
|
|
preambleRISCV []byte
|
|
preambleRISCVErr error
|
|
)
|
|
|
|
func toolchainObjectPreambleRISCV() ([]byte, error) {
|
|
preambleRISCVOnce.Do(func() {
|
|
goBin, err := exec.LookPath("go")
|
|
if err != nil {
|
|
preambleRISCVErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
|
return
|
|
}
|
|
dir, err := os.MkdirTemp("", "gasm-preamble-riscv")
|
|
if err != nil {
|
|
preambleRISCVErr = err
|
|
return
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
src := filepath.Join(dir, "probe_riscv64.s")
|
|
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
|
preambleRISCVErr = err
|
|
return
|
|
}
|
|
obj := filepath.Join(dir, "probe.o")
|
|
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
|
cmd.Env = append(os.Environ(), "GOARCH=riscv64")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
preambleRISCVErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
|
return
|
|
}
|
|
data, err := os.ReadFile(obj)
|
|
if err != nil {
|
|
preambleRISCVErr = err
|
|
return
|
|
}
|
|
i := bytes.Index(data, []byte("\n!\n"))
|
|
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
|
preambleRISCVErr = fmt.Errorf("unrecognised assembler object layout")
|
|
return
|
|
}
|
|
preambleRISCV = data[:i+3]
|
|
})
|
|
return preambleRISCV, preambleRISCVErr
|
|
}
|