diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a24e6d..1e1ddad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/asm/elfriscv.go b/asm/elfriscv.go index 779562f..7378d0c 100644 --- a/asm/elfriscv.go +++ b/asm/elfriscv.go @@ -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), - }) } } diff --git a/asm/goobj.go b/asm/goobj.go index a5c37fe..66855a6 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -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)) diff --git a/asm/goobjloong64.go b/asm/goobjloong64.go index 14578c8..92e6a11 100644 --- a/asm/goobjloong64.go +++ b/asm/goobjloong64.go @@ -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 }) } diff --git a/asm/goobjriscv.go b/asm/goobjriscv.go index 5463b4e..a8a98d3 100644 --- a/asm/goobjriscv.go +++ b/asm/goobjriscv.go @@ -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, - }) - } - 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 + return relocRISCVPcrelItype, 8 + }) } -// 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 diff --git a/asm/link.go b/asm/link.go index 53484d9..0bafa1d 100644 --- a/asm/link.go +++ b/asm/link.go @@ -89,13 +89,12 @@ func (fl *FuncLayout) LineAt(offset int) int { 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) - RelPCRelAbs // 32-bit absolute (R_RISCV_32) - RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) - RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) + RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64) + 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) ) type Reloc struct { diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 6999168..ebd2eb6 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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. diff --git a/asm/riscv_goobj_test.go b/asm/riscv_goobj_test.go new file mode 100644 index 0000000..8277936 --- /dev/null +++ b/asm/riscv_goobj_test.go @@ -0,0 +1,263 @@ +// Copyright (c) 2026 Petr Balvín (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) + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 54ac29c..a35100b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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