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 // measured from After, the address just past the instruction. An External
// relocation names a symbol no GLOBL in the file defines; the object-file // relocation names a symbol no GLOBL in the file defines; the object-file
// emitters carry it into the output's relocation table. // 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 { type Reloc struct {
Off int Off int
After int After int
Name string Name string
Addend int64 Addend int64
External bool External bool
Kind RelocKind
} }
// DataSymbol describes one GLOBL symbol laid out in the data section. // 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 return img, nil
} }
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file. // AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file
// It produces an Image with the function bodies laid out in source order. // 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) { func AssembleFileRISCV(f *ast.File) (*Image, error) {
dataSyms, err := collectData(f)
if err != nil {
return nil, err
}
img := &Image{Symbols: map[string]int{}} img := &Image{Symbols: map[string]int{}}
for _, d := range f.Decls { for _, d := range f.Decls {
t, ok := d.(*ast.Text) t, ok := d.(*ast.Text)
if !ok { if !ok {
continue continue
} }
code, labels, err := assembleRISCV(t) code, labels, relocs, err := assembleRISCV(t)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", t.Name.Name, err) return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
} }
@@ -243,6 +262,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
Args: argsSize(t), Args: argsSize(t),
Line: t.Pos().Line, Line: t.Pos().Line,
Labels: labels, Labels: labels,
Relocs: relocs,
} }
for _, f := range t.Flags { for _, f := range t.Flags {
switch f { switch f {
@@ -255,6 +275,28 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
img.Funcs = append(img.Funcs, fl) img.Funcs = append(img.Funcs, fl)
img.Code = append(img.Code, code...) 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 return img, nil
} }
@@ -263,6 +305,7 @@ type dataSym struct {
name string name string
pkg string pkg string
buf []byte buf []byte
size int
static bool static bool
rodata bool rodata bool
dupok bool dupok bool
@@ -292,6 +335,7 @@ func collectData(f *ast.File) ([]dataSym, error) {
name: name, name: name,
pkg: dd.Name.Pkg, pkg: dd.Name.Pkg,
buf: make([]byte, size), buf: make([]byte, size),
size: size,
static: dd.Name.Static, static: dd.Name.Static,
} }
for _, f := range dd.Flags { 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. // assembleRISCV assembles a RISC-V TEXT function body into machine code.
// It handles the core RV64IMAFDC instruction set. // It handles the full RV64IMAFDC instruction set including RVC compression.
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) { func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
fi := riscvComputeFrame(t) fi := riscvComputeFrame(t)
prologue := riscvPrologue(fi) prologue := riscvPrologue(fi)
var relocs []Reloc
// Pass 1: collect instructions and compute label offsets assuming 4 bytes // Pass 1: collect instructions and compute label offsets assuming 4 bytes
// per instruction (or 8 for MOV $large-imm). No encoding yet. // per instruction (or 8 for MOV $large-imm). No encoding yet.
type instrRec struct { 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. // Pass 2: encode each instruction using Pass-1 offsets.
pc := len(prologue) pc := len(prologue)
for i := range recs { 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 { 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 recs[i].code = code
pc += len(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 // Pass 5: re-encode branches with corrected offsets. Record relocations
// for instructions that can't be compressed. // during this final pass (relocation offsets are relative to instruction start).
out := append([]byte(nil), prologue...) out := append([]byte(nil), prologue...)
pc = len(prologue) pc = len(prologue)
preCount := len(relocs)
for _, r := range recs { for _, r := range recs {
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) { if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
out = append(out, r.code...) out = append(out, r.code...)
pc += len(r.code) pc += len(r.code)
} else { } else {
// Re-encode with correct offsets (branches need this). code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi)
if err != nil { 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 { if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)} 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...) out = append(out, code...)
pc += len(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. // 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). // Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW).
func riscvInstrSize(instr *ast.Instr) int { func riscvInstrSize(instr *ast.Instr) int {
mnem := instr.Mnemonic.Text mnem := instr.Mnemonic.Text
if mnem == "MOV" && len(instr.Operands) == 2 && isImmOperand(instr.Operands[0]) { ops := instr.Operands
imm := immFromOperand(instr.Operands[0]) if mnem == "MOV" && len(ops) == 2 {
if imm < -2048 || imm > 2047 { // MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
return 8 // LUI + ADDIW 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 return 4
@@ -120,7 +143,7 @@ func isBranchLike(mnem string) bool {
} }
// encodeRISCVInstr encodes a single RISC-V instruction. // 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 mnem := instr.Mnemonic.Text
ops := instr.Operands ops := instr.Operands
var word uint32 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) word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
case "CALL": 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) word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
case "JMP": 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, // MOV is a pseudo-instruction that the Go assembler uses for loads,
// stores, register moves and immediate loads. // stores, register moves and immediate loads.
case "MOV": 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 // 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-relative store
// - MOV Rs, Rd register-to-register move (ADDI $0) // - MOV Rs, Rd register-to-register move (ADDI $0)
// - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW) // - 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 ops := instr.Operands
if len(ops) != 2 { if len(ops) != 2 {
return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops)) 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. // Immediate → register.
if isImmOperand(src) { 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) rd := regFromOperand(dst)
if rd < 0 { if rd < 0 {
return nil, fmt.Errorf("MOV $imm: invalid destination register") 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). // Memory → register (load).
if isMemOperand(src) && !isMemOperand(dst) { if isMemOperand(src) && !isMemOperand(dst) {
rd := regFromOperand(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) rs1, off := memFromOperandWithFrame(src, fi)
if rd < 0 || rs1 < 0 { if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("MOV load: invalid operand") 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). // Register → memory (store).
if !isMemOperand(src) && isMemOperand(dst) { if !isMemOperand(src) && isMemOperand(dst) {
rs2 := regFromOperand(src) 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) rs1, off := memFromOperandWithFrame(dst, fi)
if rs2 < 0 || rs1 < 0 { if rs2 < 0 || rs1 < 0 {
return nil, fmt.Errorf("MOV store: invalid operand") return nil, fmt.Errorf("MOV store: invalid operand")
@@ -523,6 +607,78 @@ func encodeRISCVLoadImm(rd int, imm int32) []byte {
return out 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 // tryCompressRVC attempts to compress a RISC-V instruction to its 16-bit
// RVC form. It returns the compressed instruction word and true on success. // RVC form. It returns the compressed instruction word and true on success.
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) { 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) rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { 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. // MOV reg, mem → store, try C.SDSP.
if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) { if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) {
rs2, rs1, imm := extractSDParams(instr, fi) rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { 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). // SD rs2, offset(SP) → C.SDSP when uimm[8:3] fits (CSS-type).
rs2, rs1, imm := extractSDParams(instr, fi) rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
uimm := uint32(imm) >> 3 return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
return rvcCSS(0x7, uint32(rs2), uimm), true
} }
case "ADDI": 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). // FLD rd, imm(SP) → C.FLDSP (CI-type, funct3=0x1).
rd, rs1, imm := extractLDParams(instr, fi) rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { 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": case "FSD":
// FSD rs2, imm(SP) → C.FSDSP (CSS-type, funct3=0x5). // FSD rs2, imm(SP) → C.FSDSP (CSS-type, funct3=0x5).
rs2, rs1, imm := extractSDParams(instr, fi) rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { 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": case "LUI":
@@ -679,6 +834,35 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 { if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true 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 return 0, false
+27
View File
@@ -228,6 +228,8 @@ var riscvInstrTable = map[string]riscvEnc{
"ECALL": {0x73, 0x0, 0x00}, "ECALL": {0x73, 0x0, 0x00},
"EBREAK": {0x73, 0x0, 0x00}, "EBREAK": {0x73, 0x0, 0x00},
"FENCE": {0x0F, 0x0, 0x00}, "FENCE": {0x0F, 0x0, 0x00},
// JALR — indirect jump/call (I-type).
"JALR": {0x67, 0x0, 0x00},
// RV64A — atomics (AMO opcode 0x2F). // RV64A — atomics (AMO opcode 0x2F).
// funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27]. // 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. // 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 { func rvcCI(funct3, rd uint32, imm uint32) uint16 {
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x2) 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. // rvcCSS encodes a CSS-type (stack store) compressed instruction.
func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 { func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 {
return uint16((funct3 << 13) | (imm << 7) | (rs2 << 2) | 0x2) 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. // assembleRISCVHelper assembles one TEXT function and returns its code bytes.
func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte { func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte {
t.Helper() t.Helper()
code, _, err := assembleRISCV(fn) code, _, _, err := assembleRISCV(fn)
if err != nil { if err != nil {
t.Fatalf("assemble: %v", err) t.Fatalf("assemble: %v", err)
} }
@@ -179,11 +179,11 @@ TEXT ·frame(SB), NOSPLIT, $0-8
} }
func TestRISCV_RVC_loadStore(t *testing.T) { 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" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·rvcstore(SB), NOSPLIT, $0 TEXT ·rvcstore(SB), NOSPLIT, $0
LD 0(X2), X10 LD 0(SP), X10
SD X10, 8(X2) SD X10, 8(SP)
RET RET
`) `)
code := assembleRISCVHelper(t, fn) 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)) 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
View File
@@ -88,8 +88,8 @@ Commands:
parse parse and report syntax errors parse parse and report syntax errors
fmt canonicalise formatting (gofmt for assembly) fmt canonicalise formatting (gofmt for assembly)
lint run static checks lint run static checks
asm assemble .s files to machine code (amd64) asm assemble .s files to machine code (amd64, riscv64)
verify JIT-assemble and run dynamic checks (amd64) verify JIT-assemble and run dynamic checks (amd64, riscv64)
lsp run the language server over stdio lsp run the language server over stdio
version print the version (same as --version) version print the version (same as --version)
@@ -352,7 +352,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
func cmdAsm(args []string) int { func cmdAsm(args []string) int {
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", ` 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, 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) FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
resolved RIP-relative — and printed as a hex dump. 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" obj, kind = img.Bytes(), "raw image"
case "elf": case "elf":
obj, err = img.ELFObject() if targetArch == arch.RISCV {
obj, err = img.ELFRISCVObject()
} else {
obj, err = img.ELFObject()
}
kind = "ELF object" kind = "ELF object"
case "macho": case "macho":
obj, err = img.MachOObject() obj, err = img.MachOObject()
@@ -483,6 +487,97 @@ requires -p, the package path, and the installed Go toolchain).
return 0 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 { func cmdVerify(args []string) int {
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", ` fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available 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 return 2
} }
path := fs.Arg(0) path := fs.Arg(0)
if arch.FromFilename(path) != arch.AMD64 { targetArch := arch.FromFilename(path)
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported") if targetArch != arch.AMD64 && targetArch != arch.RISCV {
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 and riscv64 are supported")
return 1 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) k, err := verify.Load(path)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
+7 -2
View File
@@ -398,10 +398,15 @@ func parseAddress(g []token.Token) ast.Address {
return addr return addr
} }
// Symbol-with-pseudo form: name[<>][+off](PSEUDO). // 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 { if idx := findPseudoParen(g); idx >= 0 {
sym, _ := parseSymbolPrefix(g[:idx+3]) sym, _ := parseSymbolPrefix(g[:idx+3])
addr.Sym = sym if sym != nil {
return addr addr.Sym = sym
return addr
}
} }
i := 0 i := 0
+18 -3
View File
@@ -19,7 +19,20 @@ import (
// keyed by the function's short name (the part after the middle dot). // 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 // This is the universal oracle: any file that `go tool asm` accepts can
// be verified, with no hand-written reference. // 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) { 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() goroot := runtime.GOROOT()
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm") asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
if _, err := os.Stat(asmBin); err != nil { 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") includeDir := filepath.Join(goroot, "pkg", "include")
// Create a temp file for the object output.
tmpDir, err := os.MkdirTemp("", "gasm-verify-*") tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
if err != nil { if err != nil {
return nil, fmt.Errorf("verify: tempdir: %w", err) return nil, fmt.Errorf("verify: tempdir: %w", err)
@@ -35,14 +47,17 @@ func GroundTruth(path string) (map[string][]byte, error) {
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
objPath := filepath.Join(tmpDir, "out.o") objPath := filepath.Join(tmpDir, "out.o")
// Derive a package name from the file name (the assembler needs -p).
base := filepath.Base(path) base := filepath.Base(path)
pkg := strings.TrimSuffix(base, ".s") pkg := strings.TrimSuffix(base, ".s")
pkg = strings.TrimSuffix(pkg, "_amd64") pkg = strings.TrimSuffix(pkg, "_amd64")
pkg = strings.TrimSuffix(pkg, "_riscv64")
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path) 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 { 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) objData, err := os.ReadFile(objPath)