feat(asm): add RISC-V ELF relocatable object emission and SB relocation

support
This commit is contained in:
2026-08-03 08:51:00 +02:00
parent 7721353d44
commit f41a86b660
8 changed files with 824 additions and 43 deletions
+232
View File
@@ -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
View File
@@ -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
View File
@@ -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
+27
View File
@@ -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
View File
@@ -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))
}
}