feat(asm): emit RISC-V GOOBJ with the shared emitter

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-13 12:07:29 +02:00
parent 31ee8e7941
commit 9b5c878f9e
9 changed files with 358 additions and 383 deletions
+8
View File
@@ -26,6 +26,14 @@ Unreleased changes on the `development` branch.
in the architecture's MinLC units as the runtime expects — the amd64 link
test now genuinely substitutes the gasm object, and the amd64/loong64
end-to-end GOOBJ link tests pass.
- **RISC-V GOOBJ emission via the shared emitter.** RISC-V GOOBJ output is
now written by the same shared emitter as amd64 and LoongArch, modelling
each AUIPC + second-instruction pair as a single R_RISCV_PCREL_ITYPE/STYPE
relocation (the layout `cmd/asm` writes, not the ELF HI20/LO12 pair), so the
object links into a cross-compiled `go build` for `GOARCH=riscv64`. An
end-to-end link test substitutes the gasm object and reads the symbol back
with `go tool nm`; the rewrite also corrects the relocation `after` field
and the compressed `RET` encoding (C.JR ra).
### Fixed
+17 -18
View File
@@ -79,11 +79,10 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
symIdx[s.name] = i
}
// Build relocations. Each SB reference produces a pair:
// AUIPC rd, 0 → R_RISCV_PCREL_HI20
// ADDI/LD/SD → R_RISCV_PCREL_LO12_I or _S
// For now we record them as individual entries; at link time
// the linker must pair HI20 with its matching LO12.
// Build relocations. Each SB reference is an AUIPC + second-instruction
// pair carrying a single relocation kind; the ELF writer expands it into
// the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects,
// with the same addend on both.
type elfRela struct {
off uint64
typ uint32
@@ -97,22 +96,22 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
if !ok {
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
}
// Determine relocation type from the relocation kind.
typ := uint32(rRISCVPCRELHI20) // default: AUIPC
switch r.Kind {
case RelPCRelLO12:
typ = rRISCVPCRELLO12I
case RelPCRelLO12S:
typ = rRISCVPCRELLO12S
case RelRISCVPCRELIType:
relas = append(relas,
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: r.Addend},
)
case RelRISCVPCRELSType:
relas = append(relas,
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: r.Addend},
)
case RelPCRelAbs:
typ = rRISCV32
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend})
default:
return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind)
}
relas = append(relas, elfRela{
off: uint64(fn.Offset + r.Off),
typ: typ,
sym: idx,
addend: r.Addend - int64(r.After-r.Off),
})
}
}
+20 -10
View File
@@ -143,14 +143,15 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
return nil, err
}
// amd64: MinLC 1, R_PCREL for the code relocations.
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) uint16 { return relocPCRel })
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
}
// emitGOObject assembles the GOOBJ payload for any architecture. pre is
// the toolchain's object preamble; minLC is the architecture's minimum
// instruction length, the unit of the pc-value table deltas; relocType
// maps a code relocation to its objabi relocation type.
func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, relocType func(Reloc) uint16) ([]byte, error) {
// instruction length, the unit of the pc-value table deltas; relocField
// maps a code relocation to its objabi relocation type and the width of
// the instruction field the linker writes.
func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, relocField func(Reloc) (uint16, uint8)) ([]byte, error) {
if pkgPath == "" {
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
}
@@ -197,8 +198,16 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
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.
// Only the amd64 encoder resolves file-local static symbols
// into a disp32 field at assemble time; GOOBJ must leave that
// field zero for the linker to fill. The RISC-V and LoongArch
// encoders emit zero immediates with a relocation instead, and
// their relocations cover whole AUIPC/pcalau12i pairs, so
// zeroing r.Off would erase the opcode/register bits the linker
// preserves when it patches only the immediate.
if r.Kind != RelPCRel32 {
continue
}
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
}
@@ -290,6 +299,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
for i, fn := range img.Funcs {
si := len(defs) + fnNpIdx[i]
for _, r := range fn.Relocs {
typ, size := relocField(r)
if r.External {
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
pkg, name := splitQualified(r.Name)
@@ -306,8 +316,8 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
}
var rec [23]byte
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
rec[4] = 4 // field width
binary.LittleEndian.PutUint16(rec[5:], relocType(r))
rec[4] = size // field width
binary.LittleEndian.PutUint16(rec[5:], typ)
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
@@ -320,8 +330,8 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
}
var rec [23]byte
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
rec[4] = 4 // field width
binary.LittleEndian.PutUint16(rec[5:], relocType(r))
rec[4] = size // field width
binary.LittleEndian.PutUint16(rec[5:], typ)
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
+3 -3
View File
@@ -23,13 +23,13 @@ func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
if err != nil {
return nil, err
}
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) uint16 {
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
// A pcalau12i+addi.d pair: the high part carries
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO.
if r.Kind == RelLoong64AddrLo {
return relocLoong64AddrLo
return relocLoong64AddrLo, 4
}
return relocLoong64AddrHi
return relocLoong64AddrHi, 4
})
}
+19 -326
View File
@@ -5,7 +5,6 @@ package asm
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
@@ -13,343 +12,37 @@ import (
"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.
// GOObjectRISCV emits a GOOBJ object file for RISC-V. The layout is the
// shared one in goobj.go — the toolchain preamble, the go120ld header with
// its block offsets, the string table, the symbol definitions and the
// reloc/aux/data index arrays — with the RISC-V preamble, the MinLC of 2 for
// the pc-value deltas, and the single R_RISCV_PCREL_ITYPE/STYPE relocation
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
// relocation, not the ELF HI20/LO12 pair).
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
return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) {
if r.Kind == RelRISCVPCRELSType {
return relocRISCVPcrelStype, 8
}
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, 2), &pcIdx[i].sp},
{pcValueFlat(0, fn.Size, 2), &pcIdx[i].file},
{pcValueFlat(int32(fn.Line), fn.Size, 2), &pcIdx[i].line},
{pcValueFlat(-1, fn.Size, 2), &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,
return relocRISCVPcrelItype, 8
})
}
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).
// RISC-V relocation types (cmd/internal/objabi). The Go linker applies
// R_RISCV_PCREL_ITYPE/STYPE to an AUIPC + I/S-type instruction pair as a
// single 8-byte field.
const (
relocRISCVPcrelHi20 = 23
relocRISCVPcrelLo12I = 24
relocRISCVPcrelLo12S = 25
relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE
relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE
)
// toolchainObjectPreambleRISCV returns the RISC-V object preamble.
// toolchainObjectPreambleRISCV returns the "go object ...\n!\n" header
// the installed go tool asm writes for riscv64, captured by assembling a
// one-instruction probe (see toolchainObjectPreamble).
var (
preambleRISCVOnce sync.Once
preambleRISCV []byte
+2 -3
View File
@@ -90,9 +90,8 @@ type RelocKind int
const (
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
RelPCRelHI20 // R_RISCV_PCREL_HI20 (AUIPC)
RelPCRelLO12 // R_RISCV_PCREL_LO12_I (ADDI, LD)
RelPCRelLO12S // R_RISCV_PCREL_LO12_S (SD)
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
+15 -14
View File
@@ -91,9 +91,12 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
code = []byte{byte(c16), byte(c16 >> 8)}
}
// Make newly added relocation offsets absolute (subtract prologue to make
// them function-relative, then the caller adds fn.Offset).
// them function-relative, then the caller adds fn.Offset). After
// points just past the AUIPC+second-instruction pair, which is
// always 8 bytes wide for these static-symbol references.
for j := preCount; j < len(relocs); j++ {
relocs[j].Off += pc - len(prologue)
relocs[j].After = relocs[j].Off + 8
}
preCount = len(relocs)
out = append(out, code...)
@@ -608,39 +611,37 @@ func encodeRISCVLoadImm(rd int, imm int32) []byte {
}
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
// symbol into rd. Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I relocs.
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
// toolchain uses for the pair (the object-file emitters expand or map it).
func encodeRISCVSBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12})
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
addi := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rd, 0)
return append(wordLE(auipc), wordLE(addi)...)
}
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd.
// Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I relocs.
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd,
// recording the single R_RISCV_PCREL_ITYPE relocation for the pair.
func encodeRISCVSBLoad(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12})
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
ld := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rd, 0)
return append(wordLE(auipc), wordLE(ld)...)
}
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol.
// Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_S relocs.
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol,
// recording the single R_RISCV_PCREL_STYPE relocation for the pair.
func encodeRISCVSBStore(sym *ast.Symbol, rs2 int, relocs *[]Reloc) []byte {
tmp := 31 // X31 = T6
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12S})
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELSType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, tmp, 0)
sd := riscvSType(riscvEnc{0x23, 0x3, 0x00}, tmp, rs2, 0)
@@ -687,8 +688,8 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
switch mnem {
case "RET":
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rd=0, rs2=1)
return rvcCR(0x8, 0, 1), true
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rs1=ra, rs2=0).
return rvcCR(0x8, 1, 0), true
case "LD", "MOV":
// LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits.
+263
View File
@@ -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)
}
}
+6 -4
View File
@@ -299,10 +299,12 @@ boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a
gasm-assembled object drops into a `go build` in place of the toolchain's.
The object preamble (the version-and-experiment header the linker compares
verbatim) is captured from the installed `go tool asm`, so the output is
always consistent with the toolchain that links it. LoongArch GOOBJ emission
uses the same shared emitter with the loong64 marker and R_LOONG64_ADDR_HI/LO
relocation types, and the emitted object links into a real `go build` for
`GOARCH=loong64`. Per function, the emitter also writes the two DWARF
always consistent with the toolchain that links it. RISC-V and LoongArch
GOOBJ emission share this emitter: the loong64 marker with
R_LOONG64_ADDR_HI/LO relocation types, and the riscv64 marker with a single
R_RISCV_PCREL_ITYPE/STYPE relocation per AUIPC pair — the model `cmd/asm`
writes, not the ELF HI20/LO12 pair — and both link into a real `go build` for
their `GOARCH`. Per function, the emitter also writes the two DWARF
symbols the linker's DWARF pass reads verbatim — the subprogram DIE
(`SDWARFFCN`) and the `.debug_line` state-machine program (`SDWARFLINES`),
both built the way `cmd/asm` builds them (the DIE carries the