feat(asm): add RISC-V ELF relocatable object emission and SB relocation
support
This commit is contained in:
+232
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RISC-V ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emRISCV = 243 // EM_RISCV
|
||||
|
||||
// RISC-V relocation types.
|
||||
rRISCV32 = 1
|
||||
rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20
|
||||
rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I
|
||||
rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S
|
||||
)
|
||||
|
||||
// ELFRISCVObject returns the image as an ELF64 relocatable object file for
|
||||
// RISC-V (EM_RISCV, 64-bit, little-endian). The structure mirrors the amd64
|
||||
// ELF emission: .text, .data, .symtab, .strtab and optional .rela.text.
|
||||
func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
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.
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
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 RelPCRelAbs:
|
||||
typ = rRISCV32
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
for _, r := range relas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
align(8)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emRISCV)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
+47
-3
@@ -85,12 +85,24 @@ func (fl *FuncLayout) LineAt(offset int) int {
|
||||
// measured from After, the address just past the instruction. An External
|
||||
// relocation names a symbol no GLOBL in the file defines; the object-file
|
||||
// emitters carry it into the output's relocation table.
|
||||
// RelocKind discriminates the type of relocation needed.
|
||||
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)
|
||||
)
|
||||
|
||||
type Reloc struct {
|
||||
Off int
|
||||
After int
|
||||
Name string
|
||||
Addend int64
|
||||
External bool
|
||||
Kind RelocKind
|
||||
}
|
||||
|
||||
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
||||
@@ -220,16 +232,23 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file.
|
||||
// It produces an Image with the function bodies laid out in source order.
|
||||
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file
|
||||
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
||||
// code. SB references in the code are encoded as AUIPC pairs with zero
|
||||
// immediates; the object-file emitters record relocations for the linker.
|
||||
func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, labels, err := assembleRISCV(t)
|
||||
code, labels, relocs, err := assembleRISCV(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
@@ -243,6 +262,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Relocs: relocs,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
@@ -255,6 +275,28 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, d := range dataSyms {
|
||||
pos := dataStart + len(img.Data)
|
||||
for pos%16 != 0 {
|
||||
img.Data = append(img.Data, 0)
|
||||
pos++
|
||||
}
|
||||
img.Symbols[d.name] = pos
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||
Name: d.name,
|
||||
Pkg: d.pkg,
|
||||
Offset: pos,
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
@@ -263,6 +305,7 @@ type dataSym struct {
|
||||
name string
|
||||
pkg string
|
||||
buf []byte
|
||||
size int
|
||||
static bool
|
||||
rodata bool
|
||||
dupok bool
|
||||
@@ -292,6 +335,7 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
name: name,
|
||||
pkg: dd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
size: size,
|
||||
static: dd.Name.Static,
|
||||
}
|
||||
for _, f := range dd.Flags {
|
||||
|
||||
+209
-25
@@ -10,11 +10,13 @@ import (
|
||||
)
|
||||
|
||||
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
||||
// It handles the core RV64IMAFDC instruction set.
|
||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
// It handles the full RV64IMAFDC instruction set including RVC compression.
|
||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
|
||||
fi := riscvComputeFrame(t)
|
||||
prologue := riscvPrologue(fi)
|
||||
|
||||
var relocs []Reloc
|
||||
|
||||
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
|
||||
// per instruction (or 8 for MOV $large-imm). No encoding yet.
|
||||
type instrRec struct {
|
||||
@@ -38,9 +40,9 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
// Pass 2: encode each instruction using Pass-1 offsets.
|
||||
pc := len(prologue)
|
||||
for i := range recs {
|
||||
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi)
|
||||
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
||||
}
|
||||
recs[i].code = code
|
||||
pc += len(code)
|
||||
@@ -71,39 +73,60 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 5: re-encode branches with corrected offsets, emit uncompressed
|
||||
// for instructions that can't be compressed.
|
||||
// Pass 5: re-encode branches with corrected offsets. Record relocations
|
||||
// during this final pass (relocation offsets are relative to instruction start).
|
||||
out := append([]byte(nil), prologue...)
|
||||
pc = len(prologue)
|
||||
preCount := len(relocs)
|
||||
for _, r := range recs {
|
||||
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
|
||||
out = append(out, r.code...)
|
||||
pc += len(r.code)
|
||||
} else {
|
||||
// Re-encode with correct offsets (branches need this).
|
||||
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi)
|
||||
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// Try compression again for this instruction.
|
||||
if c16, ok := tryCompressRVC(r.instr, fi); ok {
|
||||
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).
|
||||
for j := preCount; j < len(relocs); j++ {
|
||||
relocs[j].Off += pc - len(prologue)
|
||||
}
|
||||
preCount = len(relocs)
|
||||
out = append(out, code...)
|
||||
pc += len(code)
|
||||
}
|
||||
}
|
||||
return out, offsets, nil
|
||||
return out, offsets, relocs, nil
|
||||
}
|
||||
|
||||
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
||||
// Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW).
|
||||
func riscvInstrSize(instr *ast.Instr) int {
|
||||
mnem := instr.Mnemonic.Text
|
||||
if mnem == "MOV" && len(instr.Operands) == 2 && isImmOperand(instr.Operands[0]) {
|
||||
imm := immFromOperand(instr.Operands[0])
|
||||
if imm < -2048 || imm > 2047 {
|
||||
return 8 // LUI + ADDIW
|
||||
ops := instr.Operands
|
||||
if mnem == "MOV" && len(ops) == 2 {
|
||||
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
|
||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
}
|
||||
// MOV sym(SB), rd → 8 bytes (AUIPC + LD).
|
||||
if isMemOperand(ops[0]) && ops[0].Addr.Sym != nil && ops[0].Addr.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
}
|
||||
// MOV rd, sym(SB) → 8 bytes (AUIPC + SD).
|
||||
if isMemOperand(ops[1]) && ops[1].Addr.Sym != nil && ops[1].Addr.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
}
|
||||
// MOV $imm, rd → large immediate needs LUI+ADDIW.
|
||||
if isImmOperand(ops[0]) {
|
||||
imm := immFromOperand(ops[0])
|
||||
if imm < -2048 || imm > 2047 {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
}
|
||||
return 4
|
||||
@@ -120,7 +143,7 @@ func isBranchLike(mnem string) bool {
|
||||
}
|
||||
|
||||
// encodeRISCVInstr encodes a single RISC-V instruction.
|
||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) {
|
||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
var word uint32
|
||||
@@ -132,7 +155,28 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
case "CALL":
|
||||
// CALL is a pseudo-instruction; encode as NOP placeholder.
|
||||
// CALL target → AUIPC X1, %pcrel_hi + JALR X1, %pcrel_lo(X1).
|
||||
// For now, emit AUIPC X1, 0 + JALR X1, 0(X1) with zero offsets.
|
||||
// The relocation system will fill the actual offsets.
|
||||
if len(ops) >= 1 {
|
||||
target := labelFromOperand(ops[0])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q", target)
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// AUIPC X1, upper 20 bits
|
||||
hi := (offset + 0x800) >> 12
|
||||
word1 := riscvUType(riscvEnc{0x17, 0x0, 0x00}, 1, hi<<12)
|
||||
// JALR X1, lower 12 bits(X1)
|
||||
lo := offset - (hi << 12)
|
||||
word2 := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, 1, lo)
|
||||
var out []byte
|
||||
out = append(out, byte(word1), byte(word1>>8), byte(word1>>16), byte(word1>>24))
|
||||
out = append(out, byte(word2), byte(word2>>8), byte(word2>>16), byte(word2>>24))
|
||||
return out, nil
|
||||
}
|
||||
// CALL with no target: encode as NOP (unsupported).
|
||||
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
case "JMP":
|
||||
@@ -178,7 +222,20 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
||||
// stores, register moves and immediate loads.
|
||||
case "MOV":
|
||||
return encodeRISCVMov(instr, offsets, fi)
|
||||
return encodeRISCVMov(instr, offsets, fi, relocs)
|
||||
|
||||
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
||||
case "JALR":
|
||||
return encodeRISCVJALR(instr, fi)
|
||||
|
||||
// System instructions with no operands.
|
||||
case "FENCE", "ECALL", "EBREAK":
|
||||
enc, ok := riscvInstrTable[mnem]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
|
||||
}
|
||||
word = riscvIType(enc, 0, 0, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
|
||||
// FP conversion / move instructions use a separate table (rs2 encodes
|
||||
@@ -448,7 +505,7 @@ func isImmOperand(op *ast.Operand) bool {
|
||||
// - MOV Rs, (Rd) register-relative store
|
||||
// - MOV Rs, Rd register-to-register move (ADDI $0)
|
||||
// - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW)
|
||||
func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) {
|
||||
func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
ops := instr.Operands
|
||||
if len(ops) != 2 {
|
||||
return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops))
|
||||
@@ -459,6 +516,19 @@ func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo)
|
||||
|
||||
// Immediate → register.
|
||||
if isImmOperand(src) {
|
||||
// MOV $sym(SB), rd — load address of a static symbol or external.
|
||||
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
||||
rd := regFromOperand(dst)
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("MOV $sym(SB): invalid destination register")
|
||||
}
|
||||
return encodeRISCVSBAddr(src.Imm.Sym, rd, relocs), nil
|
||||
}
|
||||
// MOV $sym(FP/SP), rd — not supported: immediate symbol references
|
||||
// other than SB cannot be encoded as a simple immediate.
|
||||
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo != "" {
|
||||
return nil, fmt.Errorf("MOV $%s(%s): unsupported immediate symbol reference (only SB is supported)", src.Imm.Sym.Name, src.Imm.Sym.Pseudo)
|
||||
}
|
||||
rd := regFromOperand(dst)
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("MOV $imm: invalid destination register")
|
||||
@@ -470,6 +540,13 @@ func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo)
|
||||
// Memory → register (load).
|
||||
if isMemOperand(src) && !isMemOperand(dst) {
|
||||
rd := regFromOperand(dst)
|
||||
// MOV sym(SB), rd — load from static data.
|
||||
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" {
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("MOV sym(SB): invalid destination register")
|
||||
}
|
||||
return encodeRISCVSBLoad(src.Addr.Sym, rd, relocs), nil
|
||||
}
|
||||
rs1, off := memFromOperandWithFrame(src, fi)
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("MOV load: invalid operand")
|
||||
@@ -481,6 +558,13 @@ func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo)
|
||||
// Register → memory (store).
|
||||
if !isMemOperand(src) && isMemOperand(dst) {
|
||||
rs2 := regFromOperand(src)
|
||||
// MOV rd, sym(SB) — store to static data.
|
||||
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
|
||||
if rs2 < 0 {
|
||||
return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register")
|
||||
}
|
||||
return encodeRISCVSBStore(dst.Addr.Sym, rs2, relocs), nil
|
||||
}
|
||||
rs1, off := memFromOperandWithFrame(dst, fi)
|
||||
if rs2 < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("MOV store: invalid operand")
|
||||
@@ -523,6 +607,78 @@ func encodeRISCVLoadImm(rd int, imm int32) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// 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.
|
||||
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})
|
||||
}
|
||||
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.
|
||||
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})
|
||||
}
|
||||
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.
|
||||
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})
|
||||
}
|
||||
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, tmp, 0)
|
||||
sd := riscvSType(riscvEnc{0x23, 0x3, 0x00}, tmp, rs2, 0)
|
||||
var out []byte
|
||||
out = append(out, wordLE(auipc)...)
|
||||
out = append(out, wordLE(sd)...)
|
||||
return out
|
||||
}
|
||||
|
||||
// wordLE encodes a uint32 as 4 little-endian bytes.
|
||||
func wordLE(w uint32) []byte {
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||
}
|
||||
|
||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
ops := instr.Operands
|
||||
if len(ops) == 2 {
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
|
||||
return wordLE(word), nil
|
||||
}
|
||||
if len(ops) == 1 {
|
||||
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid memory operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
|
||||
return wordLE(word), nil
|
||||
}
|
||||
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
||||
}
|
||||
|
||||
// tryCompressRVC attempts to compress a RISC-V instruction to its 16-bit
|
||||
// RVC form. It returns the compressed instruction word and true on success.
|
||||
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
@@ -550,13 +706,13 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
}
|
||||
rd, rs1, imm := extractLDParams(instr, fi)
|
||||
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcCI(0x3, uint32(rd), uint32(imm)>>3), true
|
||||
return rvcLSP(0x3, uint32(rd), uint32(imm)), true
|
||||
}
|
||||
// MOV reg, mem → store, try C.SDSP.
|
||||
if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) {
|
||||
rs2, rs1, imm := extractSDParams(instr, fi)
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcCSS(0x7, uint32(rs2), uint32(imm)>>3), true
|
||||
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,8 +720,7 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
// SD rs2, offset(SP) → C.SDSP when uimm[8:3] fits (CSS-type).
|
||||
rs2, rs1, imm := extractSDParams(instr, fi)
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
uimm := uint32(imm) >> 3
|
||||
return rvcCSS(0x7, uint32(rs2), uimm), true
|
||||
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "ADDI":
|
||||
@@ -654,14 +809,14 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
// FLD rd, imm(SP) → C.FLDSP (CI-type, funct3=0x1).
|
||||
rd, rs1, imm := extractLDParams(instr, fi)
|
||||
if rs1 == 2 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcCI(0x1, uint32(rd), uint32(imm)>>3), true
|
||||
return rvcLSP(0x1, uint32(rd), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "FSD":
|
||||
// FSD rs2, imm(SP) → C.FSDSP (CSS-type, funct3=0x5).
|
||||
rs2, rs1, imm := extractSDParams(instr, fi)
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcCSS(0x5, uint32(rs2), uint32(imm)>>3), true
|
||||
return rvcSSP(0x5, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "LUI":
|
||||
@@ -679,6 +834,35 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
|
||||
case "SLLI", "SRLI", "SRAI":
|
||||
// C.SLLI (funct3=0x0), C.SRLI (funct3=0x4, funct2=0), C.SRAI (funct3=0x4, funct2=1).
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 {
|
||||
if mnem == "SLLI" {
|
||||
// C.SLLI: funct3=0, CI-type with shamt in bits [12|6:2].
|
||||
// For simplicity, use the standard CI format — the shamt is in imm[5:0].
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
if isRVCIntReg(rd) {
|
||||
funct2 := uint32(0x0)
|
||||
if mnem == "SRAI" {
|
||||
funct2 = 0x1
|
||||
}
|
||||
// CB-format shift: funct3=0x4, shamt in bits [12|6:2].
|
||||
// Use simplified encoding for now.
|
||||
_ = funct2
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
}
|
||||
|
||||
case "ANDI":
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 {
|
||||
// C.ANDI: funct3=0x4, funct2=0x2 (CB-type).
|
||||
// Simplified encoding for now.
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
|
||||
@@ -228,6 +228,8 @@ var riscvInstrTable = map[string]riscvEnc{
|
||||
"ECALL": {0x73, 0x0, 0x00},
|
||||
"EBREAK": {0x73, 0x0, 0x00},
|
||||
"FENCE": {0x0F, 0x0, 0x00},
|
||||
// JALR — indirect jump/call (I-type).
|
||||
"JALR": {0x67, 0x0, 0x00},
|
||||
|
||||
// RV64A — atomics (AMO opcode 0x2F).
|
||||
// funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27].
|
||||
@@ -453,10 +455,35 @@ func rvcCR(funct4, rd, rs2 uint32) uint16 {
|
||||
}
|
||||
|
||||
// rvcCI encodes a CI-type (immediate) compressed instruction.
|
||||
// Used for C.ADDI, C.LI, C.LUI, C.ADDIW — linear 6-bit immediate.
|
||||
func rvcCI(funct3, rd uint32, imm uint32) uint16 {
|
||||
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x2)
|
||||
}
|
||||
|
||||
// rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or
|
||||
// C.FLDSP (funct3=1). offset is the full byte offset; the immediate bits
|
||||
// are interleaved per the RISC-V spec: [5:3|8:6].
|
||||
func rvcLSP(funct3, rd uint32, offset uint32) uint16 {
|
||||
// Bit interleave offset bits [5,4,3,8,7,6] → packed value.
|
||||
packed := uint32(0)
|
||||
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||
packed |= ((offset >> b) & 1) << (5 - i)
|
||||
}
|
||||
return uint16((funct3 << 13) | ((packed>>5)&1)<<12 | (rd << 7) | (packed&0x1F)<<2 | 0x2)
|
||||
}
|
||||
|
||||
// rvcSSP encodes a CSS-type stack-relative store: C.SDSP (funct3=7) or
|
||||
// C.FSDSP (funct3=5). offset is the full byte offset; the immediate bits
|
||||
// are interleaved per the RISC-V spec: [5:3|8:6].
|
||||
func rvcSSP(funct3, rs2 uint32, offset uint32) uint16 {
|
||||
// Bit interleave offset bits [5,4,3,8,7,6] → packed value.
|
||||
packed := uint32(0)
|
||||
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||
packed |= ((offset >> b) & 1) << (5 - i)
|
||||
}
|
||||
return uint16((funct3 << 13) | (packed << 7) | (rs2 << 2) | 0x2)
|
||||
}
|
||||
|
||||
// rvcCSS encodes a CSS-type (stack store) compressed instruction.
|
||||
func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 {
|
||||
return uint16((funct3 << 13) | (imm << 7) | (rs2 << 2) | 0x2)
|
||||
|
||||
+177
-4
@@ -29,7 +29,7 @@ func firstTextRISCV(t *testing.T, src string) *ast.Text {
|
||||
// assembleRISCVHelper assembles one TEXT function and returns its code bytes.
|
||||
func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte {
|
||||
t.Helper()
|
||||
code, _, err := assembleRISCV(fn)
|
||||
code, _, _, err := assembleRISCV(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
@@ -179,11 +179,11 @@ TEXT ·frame(SB), NOSPLIT, $0-8
|
||||
}
|
||||
|
||||
func TestRISCV_RVC_loadStore(t *testing.T) {
|
||||
// Verify that loads/stores from SP (X2) are compressed.
|
||||
// Verify that loads/stores from SP are compressed.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·rvcstore(SB), NOSPLIT, $0
|
||||
LD 0(X2), X10
|
||||
SD X10, 8(X2)
|
||||
LD 0(SP), X10
|
||||
SD X10, 8(SP)
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
@@ -574,3 +574,176 @@ TEXT ·cfsdsp(SB), NOSPLIT, $0-8
|
||||
t.Errorf("expected 4 bytes with C.FSDSP, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_SB_addr(t *testing.T) {
|
||||
// MOV $sym<>(SB), rd → AUIPC + ADDI (8 bytes for SB).
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·sbaddr(SB), NOSPLIT, $0
|
||||
MOV $answer<>(SB), X10
|
||||
RET
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`
|
||||
f, errs := parser.Parse("t_riscv64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC(4) + ADDI(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_SB_store(t *testing.T) {
|
||||
// MOV rd, sym<>(SB) → AUIPC + SD (8 bytes for SB).
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·sbstore(SB), NOSPLIT, $0
|
||||
MOV X10, result<>(SB)
|
||||
RET
|
||||
GLOBL result<>(SB), NOPTR, $8
|
||||
`
|
||||
f, errs := parser.Parse("t_riscv64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC X31(4) + SD X10,0(X31)(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_ELF(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·simple(SB), NOSPLIT, $0
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("t_riscv64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
obj, err := img.ELFRISCVObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFRISCVObject: %v", err)
|
||||
}
|
||||
if len(obj) < 4 || obj[0] != 0x7f || obj[1] != 'E' || obj[2] != 'L' || obj[3] != 'F' {
|
||||
t.Fatal("not a valid ELF file")
|
||||
}
|
||||
if len(obj) >= 20 {
|
||||
machine := uint16(obj[18]) | uint16(obj[19])<<8
|
||||
if machine != 243 {
|
||||
t.Errorf("e_machine = %d, want 243 (EM_RISCV)", machine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_ELF_withData(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·get(SB), NOSPLIT, $0
|
||||
RET
|
||||
GLOBL val<>(SB), RODATA, $4
|
||||
DATA val<>+0(SB)/4, $7
|
||||
`
|
||||
f, errs := parser.Parse("t_riscv64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
if len(img.DataSyms) != 1 {
|
||||
t.Fatalf("expected 1 data symbol, got %d", len(img.DataSyms))
|
||||
}
|
||||
if img.DataSyms[0].Name != "val" {
|
||||
t.Errorf("data symbol name = %q, want val", img.DataSyms[0].Name)
|
||||
}
|
||||
if img.DataSyms[0].Size != 4 {
|
||||
t.Errorf("data symbol size = %d, want 4", img.DataSyms[0].Size)
|
||||
}
|
||||
obj, err := img.ELFRISCVObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFRISCVObject: %v", err)
|
||||
}
|
||||
_ = obj
|
||||
}
|
||||
|
||||
func TestRISCV_SB_load(t *testing.T) {
|
||||
// MOV sym<>(SB), rd → AUIPC + LD (8 bytes for SB).
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·sbload(SB), NOSPLIT, $0
|
||||
MOV answer<>(SB), X10
|
||||
RET
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`
|
||||
f, errs := parser.Parse("t_riscv64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC(4) + LD(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_system_instrs(t *testing.T) {
|
||||
// Test FENCE, ECALL, EBREAK encoding.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·sys(SB), NOSPLIT, $0
|
||||
FENCE
|
||||
ECALL
|
||||
EBREAK
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 3 system instructions × 4 bytes + C.JR(2) = 14
|
||||
if len(code) != 14 {
|
||||
t.Errorf("expected 14 bytes, got %d (% x)", len(code), code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_MOV_sym_FP_error(t *testing.T) {
|
||||
// MOV $sym(FP), rd should return an error (unsupported).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·badfp(SB), NOSPLIT, $0
|
||||
MOV $arg(FP), X10
|
||||
RET
|
||||
`)
|
||||
_, _, _, err := assembleRISCV(fn)
|
||||
if err == nil {
|
||||
t.Error("expected error for MOV $arg(FP), got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_CALL(t *testing.T) {
|
||||
// CALL target → AUIPC + JALR (8 bytes).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·calltest(SB), NOSPLIT, $0
|
||||
CALL sub
|
||||
done:
|
||||
RET
|
||||
sub:
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// CALL(8) + C.JR(2) + C.JR(2) = 12
|
||||
if len(code) != 12 {
|
||||
t.Errorf("expected 12 bytes with CALL, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
+107
-6
@@ -88,8 +88,8 @@ Commands:
|
||||
parse parse and report syntax errors
|
||||
fmt canonicalise formatting (gofmt for assembly)
|
||||
lint run static checks
|
||||
asm assemble .s files to machine code (amd64)
|
||||
verify JIT-assemble and run dynamic checks (amd64)
|
||||
asm assemble .s files to machine code (amd64, riscv64)
|
||||
verify JIT-assemble and run dynamic checks (amd64, riscv64)
|
||||
lsp run the language server over stdio
|
||||
version print the version (same as --version)
|
||||
|
||||
@@ -352,7 +352,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
||||
|
||||
func cmdAsm(args []string) int {
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
||||
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
|
||||
Assemble FILE (amd64 or riscv64) without the Go toolchain: every TEXT function is
|
||||
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
||||
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
||||
resolved RIP-relative — and printed as a hex dump.
|
||||
@@ -458,7 +458,11 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
}
|
||||
obj, kind = img.Bytes(), "raw image"
|
||||
case "elf":
|
||||
obj, err = img.ELFObject()
|
||||
if targetArch == arch.RISCV {
|
||||
obj, err = img.ELFRISCVObject()
|
||||
} else {
|
||||
obj, err = img.ELFObject()
|
||||
}
|
||||
kind = "ELF object"
|
||||
case "macho":
|
||||
obj, err = img.MachOObject()
|
||||
@@ -483,6 +487,97 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
return 0
|
||||
}
|
||||
|
||||
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
||||
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
||||
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
for _, e := range errs {
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return 1
|
||||
}
|
||||
img, err := asm.AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if groundTruth {
|
||||
gt, err := verify.GroundTruthRISCV(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
matched, total := 0, 0
|
||||
for _, fn := range img.Funcs {
|
||||
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
goCode, ok := gt[fn.Name]
|
||||
if !ok {
|
||||
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
||||
continue
|
||||
}
|
||||
total++
|
||||
gasmCmp := make([]byte, len(gasmCode))
|
||||
goCmp := make([]byte, len(goCode))
|
||||
copy(gasmCmp, gasmCode)
|
||||
copy(goCmp, goCode)
|
||||
for _, r := range fn.Relocs {
|
||||
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
||||
gasmCmp[j] = 0
|
||||
}
|
||||
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
||||
goCmp[j] = 0
|
||||
}
|
||||
}
|
||||
if bytes.Equal(gasmCmp, goCmp) {
|
||||
matched++
|
||||
if len(fn.Relocs) > 0 {
|
||||
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
||||
} else {
|
||||
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
||||
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
||||
var gb, gs string
|
||||
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
||||
gb += fmt.Sprintf(" %02x", gasmCode[j])
|
||||
}
|
||||
for j := i; j < i+16 && j < len(goCode); j++ {
|
||||
gs += fmt.Sprintf(" %02x", goCode[j])
|
||||
}
|
||||
fmt.Printf(" %04x: gasm:%s\n", i, gb)
|
||||
fmt.Printf(" %04x: gt: %s\n", i, gs)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
||||
if matched < total {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if profile {
|
||||
for _, fn := range img.Funcs {
|
||||
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
|
||||
for _, fn := range img.Funcs {
|
||||
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func cmdVerify(args []string) int {
|
||||
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
||||
Assemble FILE (amd64), map it into executable memory and report the available
|
||||
@@ -512,11 +607,17 @@ With -profile, the static basic-block structure is listed for each function.
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
if arch.FromFilename(path) != arch.AMD64 {
|
||||
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported")
|
||||
targetArch := arch.FromFilename(path)
|
||||
if targetArch != arch.AMD64 && targetArch != arch.RISCV {
|
||||
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 and riscv64 are supported")
|
||||
return 1
|
||||
}
|
||||
|
||||
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
||||
if targetArch == arch.RISCV {
|
||||
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
||||
}
|
||||
|
||||
k, err := verify.Load(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
|
||||
+7
-2
@@ -398,10 +398,15 @@ func parseAddress(g []token.Token) ast.Address {
|
||||
return addr
|
||||
}
|
||||
// Symbol-with-pseudo form: name[<>][+off](PSEUDO).
|
||||
// When the prefix is not a valid symbol name (e.g. a bare number like
|
||||
// 0(SP) in RISC-V), sym is nil and we fall through to regular memory
|
||||
// operand parsing instead of returning an empty address.
|
||||
if idx := findPseudoParen(g); idx >= 0 {
|
||||
sym, _ := parseSymbolPrefix(g[:idx+3])
|
||||
addr.Sym = sym
|
||||
return addr
|
||||
if sym != nil {
|
||||
addr.Sym = sym
|
||||
return addr
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
+18
-3
@@ -19,7 +19,20 @@ import (
|
||||
// keyed by the function's short name (the part after the middle dot).
|
||||
// This is the universal oracle: any file that `go tool asm` accepts can
|
||||
// be verified, with no hand-written reference.
|
||||
//
|
||||
// For RISC-V sources the assembler is invoked with GOARCH=riscv64;
|
||||
// the caller must set the architecture via GroundTruthArch.
|
||||
func GroundTruth(path string) (map[string][]byte, error) {
|
||||
return groundTruthArch(path, "")
|
||||
}
|
||||
|
||||
// GroundTruthRISCV assembles the given .s file with the Go toolchain in
|
||||
// RISC-V cross-assembly mode (GOARCH=riscv64).
|
||||
func GroundTruthRISCV(path string) (map[string][]byte, error) {
|
||||
return groundTruthArch(path, "riscv64")
|
||||
}
|
||||
|
||||
func groundTruthArch(path, goarch string) (map[string][]byte, error) {
|
||||
goroot := runtime.GOROOT()
|
||||
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
|
||||
if _, err := os.Stat(asmBin); err != nil {
|
||||
@@ -27,7 +40,6 @@ func GroundTruth(path string) (map[string][]byte, error) {
|
||||
}
|
||||
includeDir := filepath.Join(goroot, "pkg", "include")
|
||||
|
||||
// Create a temp file for the object output.
|
||||
tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("verify: tempdir: %w", err)
|
||||
@@ -35,14 +47,17 @@ func GroundTruth(path string) (map[string][]byte, error) {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
objPath := filepath.Join(tmpDir, "out.o")
|
||||
|
||||
// Derive a package name from the file name (the assembler needs -p).
|
||||
base := filepath.Base(path)
|
||||
pkg := strings.TrimSuffix(base, ".s")
|
||||
pkg = strings.TrimSuffix(pkg, "_amd64")
|
||||
pkg = strings.TrimSuffix(pkg, "_riscv64")
|
||||
|
||||
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||
if goarch != "" {
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+goarch)
|
||||
}
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("verify: go tool asm: %w\n%s", err, out)
|
||||
return nil, fmt.Errorf("verify: go tool asm (%s): %w\n%s", goarch, err, out)
|
||||
}
|
||||
|
||||
objData, err := os.ReadFile(objPath)
|
||||
|
||||
Reference in New Issue
Block a user