fix(asm): correct RISC-V frame layout and RVC encodings

Assisted-by: GLM 5.2
This commit is contained in:
2026-08-13 14:41:57 +02:00
parent 9b5c878f9e
commit b78b6c5004
11 changed files with 489 additions and 249 deletions
+11 -2
View File
@@ -32,11 +32,20 @@ Unreleased changes on the `development` branch.
relocation (the layout `cmd/asm` writes, not the ELF HI20/LO12 pair), so the
object links into a cross-compiled `go build` for `GOARCH=riscv64`. An
end-to-end link test substitutes the gasm object and reads the symbol back
with `go tool nm`; the rewrite also corrects the relocation `after` field
and the compressed `RET` encoding (C.JR ra).
with `go tool nm`; the rewrite also corrects the relocation `after` field.
### Fixed
- **RISC-V frame model and RVC encodings.** The riscv64 frame layout now
matches `go tool asm`: the prologue/epilogue save and restore the link
register (LR) instead of S0, with the correct autosize (locals + 8) and the
RVC-compressed prologue/epilogue instructions; `RET` emits the uncompressed
`JALR X0, 0(X1)` the toolchain writes; the `C.ADDI`/`C.LI`/`C.LUI`/`C.ADDIW`
opcode bit and the `C.ADD` CR-type encoding are fixed; and the `LR`/`TMP`
register aliases now resolve to X1 and X31. The pcsp/pcfile/pcline tables
are populated from the recorded stack-adjustment and source-line data, and
a byte-exact ground-truth test compares framed and leaf functions against
`GOARCH=riscv64 go tool asm`.
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
hardware watchpoint slot 0, so a second `watch` call silently overwrote
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
+6 -4
View File
@@ -81,8 +81,10 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
// Build relocations. Each SB reference is an AUIPC + second-instruction
// pair carrying a single relocation kind; the ELF writer expands it into
// the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects,
// with the same addend on both.
// the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects.
// The HI20 carries the symbol addend; the LO12 addend is zero, matching
// cmd/link's own ELF conversion (the LO12 resolves against the HI20's
// AUIPC location).
type elfRela struct {
off uint64
typ uint32
@@ -100,12 +102,12 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
case RelRISCVPCRELIType:
relas = append(relas,
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: 0},
)
case RelRISCVPCRELSType:
relas = append(relas,
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: 0},
)
case RelPCRelAbs:
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend})
+3 -1
View File
@@ -249,7 +249,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
if !ok {
continue
}
code, labels, relocs, err := assembleRISCV(t)
code, labels, relocs, lines, spadj, err := assembleRISCV(t)
if err != nil {
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
}
@@ -263,6 +263,8 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
Args: argsSize(t),
Line: t.Pos().Line,
Labels: labels,
Lines: lines,
Spadj: spadj,
Relocs: relocs,
}
for _, f := range t.Flags {
+32 -21
View File
@@ -5,17 +5,25 @@ package asm
import (
"fmt"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
// It handles the full RV64IMAFDC instruction set including RVC compression.
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := riscvComputeFrame(t)
prologue := riscvPrologue(fi)
var relocs []Reloc
var spadj []SpadjStep
// The prologue raises the SP delta by autosize; the boundary is reported
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
if fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: riscvPrologueSpadjPC(fi), Value: fi.autosize})
}
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
// per instruction (or 8 for MOV $large-imm). No encoding yet.
@@ -33,7 +41,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
offsets[s.Name.Text] = pos
case *ast.Instr:
recs = append(recs, instrRec{instr: s})
pos += riscvInstrSize(s)
pos += riscvInstrSize(s, fi)
}
}
@@ -42,7 +50,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
for i := range recs {
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
if err != nil {
return nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
}
recs[i].code = code
pc += len(code)
@@ -78,14 +86,16 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
out := append([]byte(nil), prologue...)
pc = len(prologue)
preCount := len(relocs)
var lines []LineEntry
for _, r := range recs {
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
out = append(out, r.code...)
pc += len(r.code)
} else {
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)}
@@ -99,18 +109,26 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
relocs[j].After = relocs[j].Off + 8
}
preCount = len(relocs)
// The RET's epilogue closes the frame: the SP delta returns to zero
// after its ADDI (restore LR + ADDI).
if strings.ToUpper(r.instr.Mnemonic.Text) == "RET" && fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: pc + riscvReturnEpilogueLen(fi), Value: 0})
}
out = append(out, code...)
pc += len(code)
}
}
return out, offsets, relocs, nil
return out, offsets, relocs, lines, spadj, 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 {
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
mnem := instr.Mnemonic.Text
ops := instr.Operands
if mnem == "RET" {
return len(riscvReturn(fi))
}
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" {
@@ -154,9 +172,8 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
// Handle pseudo-instructions and special cases first.
switch mnem {
case "RET":
// RET = JALR X0, 0(X1)
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
// RET = epilogue (restore LR and close the frame when present) + C.JR ra.
return riscvReturn(fi), nil
case "CALL":
// CALL target → AUIPC X1, %pcrel_hi + JALR X1, %pcrel_lo(X1).
// For now, emit AUIPC X1, 0 + JALR X1, 0(X1) with zero offsets.
@@ -687,10 +704,6 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
ops := instr.Operands
switch mnem {
case "RET":
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rs1=ra, rs2=0).
return rvcCR(0x8, 1, 0), true
case "LD", "MOV":
// LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits.
// MOV name+off(FP), rd → load, same compression.
@@ -764,20 +777,18 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
return 0, false
case "ADD":
// ADD rd, rs2 → C.ADD when rd == rs1 and both in prime regs (rd ≠ 0).
// ADD is commutative: if rd == rs2, swap.
// ADD rd, rs2 → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD is
// commutative, so if rd == rs2, swap.
if len(ops) == 3 {
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
// C.ADD: funct6=0x27, funct2=0x0 (CA-type)
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs2)), true
if rd == rs1 && rs2 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs2)), true
}
if rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
// Swap: C.ADD rd, rs1
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs1)), true
if rd == rs2 && rs1 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs1)), true
}
}
}
+5 -5
View File
@@ -13,7 +13,7 @@ func riscvRegNum(name string) int {
// Numbered integer registers.
case "X0", "ZERO":
return 0
case "X1", "RA":
case "X1", "RA", "LR":
return 1
case "X2", "SP":
return 2
@@ -21,9 +21,9 @@ func riscvRegNum(name string) int {
return 3
case "X4", "TP":
return 4
case "X5", "T0", "LR":
case "X5", "T0":
return 5
case "X6", "T1", "TMP":
case "X6", "T1":
return 6
case "X7", "T2":
return 7
@@ -73,7 +73,7 @@ func riscvRegNum(name string) int {
return 29
case "X30", "T5":
return 30
case "X31", "T6":
case "X31", "T6", "TMP":
return 31
// Floating-point registers (F0-F31).
case "F0", "FT0":
@@ -457,7 +457,7 @@ 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)
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1)
}
// rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or
+129 -128
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)
}
@@ -65,9 +65,9 @@ TEXT ·arith(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 5 R-type instructions + RET compressed = 5*4 + 2 = 22
if len(code) != 22 {
t.Errorf("expected 22 bytes, got %d", len(code))
// 5 R-type instructions + RET = 5*4 + 4 = 24
if len(code) != 24 {
t.Errorf("expected 24 bytes, got %d", len(code))
}
}
@@ -81,9 +81,9 @@ TEXT ·mem(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 4 loads/stores (4B each) + C.JR RET (2B) = 18
if len(code) != 18 {
t.Errorf("expected 18 bytes, got %d", len(code))
// 4 loads/stores (4B each) + JALR RET (4B) = 20
if len(code) != 20 {
t.Errorf("expected 20 bytes, got %d", len(code))
}
}
@@ -97,9 +97,9 @@ TEXT ·imm(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 4 I-type + C.JR = 4*4 + 2 = 18
if len(code) != 18 {
t.Errorf("expected 18 bytes, got %d", len(code))
// 4 I-type + JALR = 4*4 + 4 = 20
if len(code) != 20 {
t.Errorf("expected 20 bytes, got %d", len(code))
}
}
@@ -129,9 +129,9 @@ TEXT ·small(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// ADDI (4B) + C.JR (2B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d", len(code))
// ADDI (4B) + JALR (4B) = 8
if len(code) != 8 {
t.Errorf("expected 8 bytes, got %d", len(code))
}
}
@@ -143,9 +143,9 @@ TEXT ·large(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// LUI (4B) + ADDIW (4B) + C.JR (2B) = 10
if len(code) != 10 {
t.Errorf("expected 10 bytes, got %d", len(code))
// LUI (4B) + ADDIW (4B) + JALR (4B) = 12
if len(code) != 12 {
t.Errorf("expected 12 bytes, got %d", len(code))
}
}
@@ -157,9 +157,9 @@ TEXT ·reg(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// C.MV (2B) + C.JR (2B) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes, got %d (% x)", len(code), code)
// C.MV (2B) + JALR (4B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d (% x)", len(code), code)
}
}
@@ -172,9 +172,9 @@ TEXT ·frame(SB), NOSPLIT, $0-8
RET
`)
code := assembleRISCVHelper(t, fn)
// C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d", len(code))
// C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8
if len(code) != 8 {
t.Errorf("expected 8 bytes, got %d", len(code))
}
}
@@ -187,9 +187,9 @@ TEXT ·rvcstore(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d (% x)", len(code), code)
// C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8
if len(code) != 8 {
t.Errorf("expected 8 bytes, got %d (% x)", len(code), code)
}
}
@@ -202,9 +202,9 @@ TEXT ·amo(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 3 AMO instructions (4B each) + C.JR (2B) = 14
if len(code) != 14 {
t.Errorf("expected 14 bytes, got %d", len(code))
// 3 AMO instructions (4B each) + JALR (4B) = 16
if len(code) != 16 {
t.Errorf("expected 16 bytes, got %d", len(code))
}
}
@@ -219,9 +219,9 @@ TEXT ·fpadd(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 5 FP instructions (4B each) + C.JR (2B) = 22
if len(code) != 22 {
t.Errorf("expected 22 bytes, got %d (%d)", len(code), len(code))
// 5 FP instructions (4B each) + JALR (4B) = 24
if len(code) != 24 {
t.Errorf("expected 24 bytes, got %d (%d)", len(code), len(code))
}
}
@@ -234,9 +234,9 @@ TEXT ·csrtest(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 3 CSR instructions (4B each) + C.JR (2B) = 14
if len(code) != 14 {
t.Errorf("expected 14 bytes, got %d", len(code))
// 3 CSR instructions (4B each) + JALR (4B) = 16
if len(code) != 16 {
t.Errorf("expected 16 bytes, got %d", len(code))
}
}
@@ -250,9 +250,9 @@ TEXT ·fmatest(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 4 FMA instructions (4B each) + C.JR (2B) = 18
if len(code) != 18 {
t.Errorf("expected 18 bytes, got %d", len(code))
// 4 FMA instructions (4B each) + JALR (4B) = 20
if len(code) != 20 {
t.Errorf("expected 20 bytes, got %d", len(code))
}
}
@@ -266,9 +266,9 @@ TEXT ·cvt(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 4 conversion instructions (4B each) + C.JR (2B) = 18
if len(code) != 18 {
t.Errorf("expected 18 bytes, got %d", len(code))
// 4 conversion instructions (4B each) + JALR (4B) = 20
if len(code) != 20 {
t.Errorf("expected 20 bytes, got %d", len(code))
}
}
@@ -281,9 +281,9 @@ TEXT ·cmp(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// 3 FP compare (4B each) + C.JR (2B) = 14
if len(code) != 14 {
t.Errorf("expected 14 bytes, got %d", len(code))
// 3 FP compare (4B each) + JALR (4B) = 16
if len(code) != 16 {
t.Errorf("expected 16 bytes, got %d", len(code))
}
}
@@ -312,9 +312,9 @@ TEXT ·caddi(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// C.ADDI (2B) + C.JR (2B) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes, got %d", len(code))
// C.ADDI (2B) + JALR (4B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d", len(code))
}
}
@@ -326,9 +326,9 @@ TEXT ·cli(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// C.LI (2B) + C.JR (2B) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes, got %d", len(code))
// C.LI (2B) + JALR (4B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d", len(code))
}
}
@@ -340,9 +340,9 @@ TEXT ·clui(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
// C.LUI (2B) + C.JR (2B) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes, got %d", len(code))
// C.LUI (2B) + JALR (4B) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes, got %d", len(code))
}
}
@@ -368,13 +368,13 @@ TEXT ·sub(SB), NOSPLIT, $0
if len(img.Funcs) != 2 {
t.Fatalf("expected 2 functions, got %d", len(img.Funcs))
}
// func add: C.LDSP(2) + C.JR(2) = 4
if img.Funcs[0].Size != 4 {
t.Errorf("add: expected 4 bytes, got %d", img.Funcs[0].Size)
// func add: C.LDSP(2) + JALR(4) = 6
if img.Funcs[0].Size != 6 {
t.Errorf("add: expected 6 bytes, got %d", img.Funcs[0].Size)
}
// func sub: SUB(4) + C.JR(2) = 6
if img.Funcs[1].Size != 6 {
t.Errorf("sub: expected 6 bytes, got %d", img.Funcs[1].Size)
// func sub: SUB(4) + JALR(4) = 8
if img.Funcs[1].Size != 8 {
t.Errorf("sub: expected 8 bytes, got %d", img.Funcs[1].Size)
}
}
@@ -384,34 +384,34 @@ func TestRISCV_encodings(t *testing.T) {
name, src string
wantBytes int
}{
{"ADD", "ADD X10, X11, X12\nRET\n", 6},
{"SUBW", "SUBW X10, X11, X12\nRET\n", 6},
{"MUL", "MUL X10, X11, X12\nRET\n", 6},
{"DIVW", "DIVW X10, X11, X12\nRET\n", 6},
{"REMUW", "REMUW X10, X11, X12\nRET\n", 6},
{"ADDIW", "ADDIW X10, $5, X11\nRET\n", 6},
{"SLLI", "SLLI X10, $3, X11\nRET\n", 6}, // ADDI+SLLI? No, SLLI uses I-type
{"SRLI", "SRLI X10, $2, X11\nRET\n", 6},
{"SRAI", "SRAI X10, $1, X11\nRET\n", 6},
{"LB", "LB (X10), X11\nRET\n", 6},
{"LBU", "LBU (X10), X11\nRET\n", 6},
{"LH", "LH (X10), X11\nRET\n", 6},
{"LHU", "LHU (X10), X11\nRET\n", 6},
{"LWU", "LWU (X10), X11\nRET\n", 6},
{"SB", "SB X10, (X11)\nRET\n", 6},
{"SH", "SH X10, (X11)\nRET\n", 6},
{"SW", "SW X10, (X11)\nRET\n", 6},
{"LUI", "LUI X10, $0x12345\nRET\n", 6},
{"AUIPC", "AUIPC X10, $0\nRET\n", 6},
{"FLW", "FLW (X10), F10\nRET\n", 6},
{"FSW", "FSW F10, (X11)\nRET\n", 6},
{"FADDS", "FADDS F10, F11, F12\nRET\n", 6},
{"FMINS", "FMINS F10, F11, F12\nRET\n", 6},
{"FMAXD", "FMAXD F10, F11, F12\nRET\n", 6},
{"FCVTSD", "FCVTSD F10, F11\nRET\n", 6},
{"FCVTDS", "FCVTDS F10, F11\nRET\n", 6},
{"FMVXW", "FMVXW F10, X10\nRET\n", 6},
{"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 6},
{"ADD", "ADD X10, X11, X12\nRET\n", 8},
{"SUBW", "SUBW X10, X11, X12\nRET\n", 8},
{"MUL", "MUL X10, X11, X12\nRET\n", 8},
{"DIVW", "DIVW X10, X11, X12\nRET\n", 8},
{"REMUW", "REMUW X10, X11, X12\nRET\n", 8},
{"ADDIW", "ADDIW X10, $5, X11\nRET\n", 8},
{"SLLI", "SLLI X10, $3, X11\nRET\n", 8}, // ADDI+SLLI? No, SLLI uses I-type
{"SRLI", "SRLI X10, $2, X11\nRET\n", 8},
{"SRAI", "SRAI X10, $1, X11\nRET\n", 8},
{"LB", "LB (X10), X11\nRET\n", 8},
{"LBU", "LBU (X10), X11\nRET\n", 8},
{"LH", "LH (X10), X11\nRET\n", 8},
{"LHU", "LHU (X10), X11\nRET\n", 8},
{"LWU", "LWU (X10), X11\nRET\n", 8},
{"SB", "SB X10, (X11)\nRET\n", 8},
{"SH", "SH X10, (X11)\nRET\n", 8},
{"SW", "SW X10, (X11)\nRET\n", 8},
{"LUI", "LUI X10, $0x12345\nRET\n", 8},
{"AUIPC", "AUIPC X10, $0\nRET\n", 8},
{"FLW", "FLW (X10), F10\nRET\n", 8},
{"FSW", "FSW F10, (X11)\nRET\n", 8},
{"FADDS", "FADDS F10, F11, F12\nRET\n", 8},
{"FMINS", "FMINS F10, F11, F12\nRET\n", 8},
{"FMAXD", "FMAXD F10, F11, F12\nRET\n", 8},
{"FCVTSD", "FCVTSD F10, F11\nRET\n", 8},
{"FCVTDS", "FCVTDS F10, F11\nRET\n", 8},
{"FMVXW", "FMVXW F10, X10\nRET\n", 8},
{"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 8},
}
for _, tt := range tests {
@@ -438,9 +438,9 @@ done:
RET
`)
code := assembleRISCVHelper(t, fn)
// C.ADDI(2) + C.BEQZ(2) + C.ADDI(2) + C.JR(2) = 8 (all compress)
if len(code) != 8 {
t.Errorf("expected 8 bytes with C.BEQZ, got %d", len(code))
// C.ADDI(2) + C.BEQZ(2) + C.ADDI(2) + JALR(4) = 10 (all compress)
if len(code) != 10 {
t.Errorf("expected 10 bytes with C.BEQZ, got %d", len(code))
}
}
@@ -453,9 +453,9 @@ func TestRISCV_RVC_CJ(t *testing.T) {
RET
`)
code := assembleRISCVHelper(t, fn)
// C.J(2) + C.JR(2) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.J, got %d", len(code))
// C.J(2) + JALR(4) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.J, got %d", len(code))
}
}
@@ -467,9 +467,9 @@ func TestRISCV_RVC_CADD(t *testing.T) {
RET
`)
code := assembleRISCVHelper(t, fn)
// C.ADD(2) + C.JR(2) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.ADD, got %d", len(code))
// C.ADD(2) + JALR(4) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.ADD, got %d", len(code))
}
}
@@ -481,9 +481,9 @@ func TestRISCV_RVC_CADD_commute(t *testing.T) {
RET
`)
code := assembleRISCVHelper(t, fn)
// C.ADD(2) + C.JR(2) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.ADD (commuted), got %d", len(code))
// C.ADD(2) + JALR(4) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.ADD (commuted), got %d", len(code))
}
}
@@ -508,8 +508,8 @@ TEXT ·csub2(SB), NOSPLIT, $0
RET
`)
code2 := assembleRISCVHelper(t, fn2)
if len(code2) != 4 {
t.Errorf("expected 4 bytes with C.SUB, got %d (% x)", len(code2), code2)
if len(code2) != 6 {
t.Errorf("expected 6 bytes with C.SUB, got %d (% x)", len(code2), code2)
}
}
@@ -520,8 +520,8 @@ TEXT ·cxor(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.XOR, got %d", len(code))
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.XOR, got %d", len(code))
}
}
@@ -532,8 +532,8 @@ TEXT ·cor(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.OR, got %d", len(code))
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.OR, got %d", len(code))
}
}
@@ -544,8 +544,8 @@ TEXT ·cand(SB), NOSPLIT, $0
RET
`)
code := assembleRISCVHelper(t, fn)
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.AND, got %d", len(code))
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.AND, got %d", len(code))
}
}
@@ -556,9 +556,9 @@ TEXT ·cfldsp(SB), NOSPLIT, $0-8
RET
`)
code := assembleRISCVHelper(t, fn)
// C.FLDSP(2) + C.JR(2) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.FLDSP, got %d", len(code))
// C.FLDSP(2) + JALR(4) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.FLDSP, got %d", len(code))
}
}
@@ -569,9 +569,9 @@ TEXT ·cfsdsp(SB), NOSPLIT, $0-8
RET
`)
code := assembleRISCVHelper(t, fn)
// C.FSDSP(2) + C.JR(2) = 4
if len(code) != 4 {
t.Errorf("expected 4 bytes with C.FSDSP, got %d", len(code))
// C.FSDSP(2) + JALR(4) = 6
if len(code) != 6 {
t.Errorf("expected 6 bytes with C.FSDSP, got %d", len(code))
}
}
@@ -592,9 +592,9 @@ DATA answer<>+0(SB)/8, $42
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)
// AUIPC(4) + ADDI(4) + JALR(4) = 12
if img.Funcs[0].Size != 12 {
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
}
}
@@ -614,9 +614,9 @@ GLOBL result<>(SB), NOPTR, $8
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)
// AUIPC X31(4) + SD X10,0(X31)(4) + JALR(4) = 12
if img.Funcs[0].Size != 12 {
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
}
}
@@ -696,9 +696,9 @@ DATA answer<>+0(SB)/8, $42
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)
// AUIPC(4) + LD(4) + JALR(4) = 12
if img.Funcs[0].Size != 12 {
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
}
}
@@ -712,9 +712,9 @@ TEXT ·sys(SB), NOSPLIT, $0
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)
// 3 system instructions × 4 bytes + JALR(4) = 16
if len(code) != 16 {
t.Errorf("expected 16 bytes, got %d (% x)", len(code), code)
}
}
@@ -725,7 +725,7 @@ TEXT ·badfp(SB), NOSPLIT, $0
MOV $arg(FP), X10
RET
`)
_, _, _, err := assembleRISCV(fn)
_, _, _, _, _, err := assembleRISCV(fn)
if err == nil {
t.Error("expected error for MOV $arg(FP), got nil")
}
@@ -742,8 +742,9 @@ 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))
// A zero-frame non-leaf function saves LR: prologue (8) + CALL (8) +
// two framed RETs (8 each) = 32.
if len(code) != 32 {
t.Errorf("expected 32 bytes with CALL, got %d", len(code))
}
}
+142 -84
View File
@@ -3,115 +3,173 @@
package asm
import "sourcedock.dev/petrbalvin/gasm-devkit/ast"
import (
"strings"
// RISC-V frame mapping: translates Go's FP/SP pseudo-register addressing
// into real RISC-V memory accesses.
//
// In Go's ABI0 (used by assembly functions), arguments are passed on the
// stack. At function entry the return address sits at SP, so the frame
// pointer FP == SP+8 and the first argument is at FP+0 == SP+8.
//
// On RISC-V the hardware registers are:
// SP = X2 (stack pointer)
// FP = S0 = X8 (frame pointer, by convention)
//
// For NOSPLIT $0 functions the prologue is omitted and arguments are read
// directly from SP+8+offset.
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
// riscvFrameInfo holds the frame parameters computed from a TEXT directive.
// RISC-V frame mapping, matching the Go toolchain's riscv64 backend.
//
// Go's riscv64 functions have no hardware frame pointer: FP and SP are
// synthetic registers resolved against the hardware stack pointer (X2) and
// the frame size. The return address lives in the link register (X1, RA/LR).
//
// The autosize is the real stack adjustment: the declared local frame plus
// the 8 bytes for the saved link register (the toolchain's FixedFrameSize).
// A leaf function with a zero frame gets no prologue at all.
//
// Prologue (autosize > 0), byte-identical to the toolchain:
//
// MOV LR, -autosize(SP) // save LR below the new SP (traceback-safe)
// ADDI $-autosize, SP, SP // open the frame
// MOV LR, 0(SP) // save LR again at SP (signal-safety)
//
// Epilogue (autosize > 0): MOV 0(SP), LR; ADDI $autosize, SP, SP; the RET's
// uncompressed JALR X0, 0(X1) follows. The toolchain restores LR on every
// frame, leaf or not.
// riscvFrameInfo holds the frame layout derived from a TEXT directive.
type riscvFrameInfo struct {
frameSize int // the $framesize from TEXT
argsSize int // the -argsize from TEXT
noSplit bool // the NOSPLIT flag
autosize int // the real SP adjustment (locals + saved LR)
}
// riscvComputeFrame extracts frame information from a TEXT directive.
// riscvComputeFrame derives the frame layout for a TEXT function.
func riscvComputeFrame(t *ast.Text) riscvFrameInfo {
fi := riscvFrameInfo{}
fi.frameSize = frameSize(t)
fi.argsSize = argsSize(t)
for _, f := range t.Flags {
if f == "NOSPLIT" {
fi.noSplit = true
frame := frameSize(t)
if frame != 0 || !riscvIsLeaf(t) {
// FixedFrameSize = 8: space for the saved link register. A
// zero-frame non-leaf function still opens an 8-byte frame for LR.
return riscvFrameInfo{autosize: frame + 8}
}
return riscvFrameInfo{}
}
// riscvIsLeaf reports whether a function contains no call instructions.
// CALL always links; JAL/JALR link only when their destination register is
// the link register (X1), matching cmd/internal/obj/riscv's containsCall.
func riscvIsLeaf(t *ast.Text) bool {
for _, stmt := range t.Body {
in, ok := stmt.(*ast.Instr)
if !ok {
continue
}
switch strings.ToUpper(in.Mnemonic.Text) {
case "CALL":
return false
case "JAL":
// JAL rd, target — a call only when rd is the link register.
if len(in.Operands) >= 2 && regFromOperand(in.Operands[0]) == 1 {
return false
}
case "JALR":
// JALR rs1, rd — a call when rd is X1; JALR offset(rs1) always
// links to X1.
if len(in.Operands) == 1 {
return false
}
if len(in.Operands) >= 2 && regFromOperand(in.Operands[1]) == 1 {
return false
}
}
}
return fi
return true
}
// riscvPrologue returns the prologue bytes for a RISC-V function.
// For NOSPLIT $0 functions there is no prologue. For functions with a
// frame, we emit: ADDI SP, SP, -framesize; SD S0, (framesize-8)(SP); ...
// riscvPrologue returns the prologue bytes for a RISC-V function, matching
// the toolchain's compression: the SP adjustment compresses to C.ADDI when
// the immediate fits, and the second LR save compresses to C.SDSP.
func riscvPrologue(fi riscvFrameInfo) []byte {
if fi.noSplit && fi.frameSize == 0 {
return nil // no prologue for NOSPLIT $0
}
var out []byte
if fi.frameSize > 0 {
// ADDI SP, SP, -framesize
out = append(out, riscvITypeLE(0x13, 0x0, 2, 2, int32(-fi.frameSize))...)
// Save the frame pointer (S0 = X8) at the top of the new frame.
// SD S0, (framesize-8)(SP)
out = append(out, riscvSTypeLE(0x23, 0x3, 2, 8, int32(fi.frameSize-8))...)
}
return out
}
// riscvEpilogue returns the epilogue bytes for a RISC-V function.
func riscvEpilogue(fi riscvFrameInfo) []byte {
if fi.noSplit && fi.frameSize == 0 {
if fi.autosize == 0 {
return nil
}
var out []byte
if fi.frameSize > 0 {
// Restore the frame pointer: LD S0, (framesize-8)(SP)
out = append(out, riscvITypeLE(0x03, 0x3, 8, 2, int32(fi.frameSize-8))...)
// ADDI SP, SP, framesize
out = append(out, riscvITypeLE(0x13, 0x0, 2, 2, int32(fi.frameSize))...)
}
// MOV LR, -autosize(SP) — SD X1, -autosize(X2). The negative offset is
// not compressible to C.SDSP (unsigned), so it stays 4 bytes.
out = append(out, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 2, 1, int32(-fi.autosize)))...)
// ADDI $-autosize, SP, SP — open the frame (C.ADDI when it fits).
out = append(out, riscvSPAdjust(int32(-fi.autosize))...)
// MOV LR, 0(SP) — SD X1, 0(X2) → C.SDSP X1, 0.
c := rvcSSP(0x7, 1, 0)
out = append(out, byte(c), byte(c>>8))
return out
}
// riscvReturn returns the bytes for a RET: the epilogue (restore LR and
// deallocate the frame when present) followed by the uncompressed JALR X0,
// 0(X1) the toolchain emits for RET (it never compresses RET to C.JR).
func riscvReturn(fi riscvFrameInfo) []byte {
var out []byte
if fi.autosize != 0 {
// MOV 0(SP), LR — LD X1, 0(X2) → C.LDSP X1, 0.
c := rvcLSP(0x3, 1, 0)
out = append(out, byte(c), byte(c>>8))
// ADDI $autosize, SP, SP — close the frame (C.ADDI when it fits).
out = append(out, riscvSPAdjust(int32(fi.autosize))...)
}
// JALR X0, 0(X1).
return append(out, wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0))...)
}
// riscvSPAdjust emits an ADDI rd, imm, rd for the stack pointer (rd = rs1 =
// X2), compressed to C.ADDI when the immediate is a nonzero 6-bit signed
// value.
func riscvSPAdjust(imm int32) []byte {
if riscvFitsCAddi(imm) {
c := rvcCI(0x0, 2, uint32(imm)&0x3F)
return []byte{byte(c), byte(c >> 8)}
}
return wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 2, 2, imm))
}
// riscvFitsCAddi reports whether imm compresses to C.ADDI (a nonzero 6-bit
// signed immediate).
func riscvFitsCAddi(imm int32) bool {
return imm != 0 && imm >= -32 && imm <= 31
}
// riscvPrologueSpadjPC returns the function-relative byte offset where the
// prologue has finished decrementing SP (the delta becomes autosize).
func riscvPrologueSpadjPC(fi riscvFrameInfo) int {
if fi.autosize == 0 {
return 0
}
// SD (4 bytes) + ADDI/C.ADDI (2 or 4 bytes).
return 4 + riscvSPAdjustLen(int32(-fi.autosize))
}
// riscvReturnEpilogueLen returns the byte length of the RET's epilogue up to
// (but not including) the final JALR — the point where SP is restored.
func riscvReturnEpilogueLen(fi riscvFrameInfo) int {
if fi.autosize == 0 {
return 0
}
// C.LDSP (2 bytes) + ADDI/C.ADDI (2 or 4 bytes).
return 2 + riscvSPAdjustLen(int32(fi.autosize))
}
func riscvSPAdjustLen(imm int32) int {
if riscvFitsCAddi(imm) {
return 2
}
return 4
}
// riscvResolvePseudo translates a pseudo-register memory reference into a
// real base register and offset. It handles name+offset(FP) and
// name+offset(SP).
//
// Returns the base register number and the adjusted offset.
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
// x+N(SP) → (N + autosize)(SP). Returns base = -1 for an unresolvable
// reference (SB: static data, handled by the relocation path).
func riscvResolvePseudo(sym *ast.Symbol, fi riscvFrameInfo) (base int, off int32) {
if sym == nil {
return -1, 0
}
offset := int32(sym.Offset)
switch sym.Pseudo {
case "FP":
// FP == SP+8 for NOSPLIT $0; arguments are at SP+8+offset.
if fi.noSplit && fi.frameSize == 0 {
return 2, 8 + offset // SP + 8 + argOffset
}
// With a frame, FP points to the saved frame; args are at FP+offset.
return 8, offset // S0 + argOffset
return 2, int32(sym.Offset) + int32(fi.autosize) + 8
case "SP":
// SP-relative; the offset is from the current SP.
return 2, offset
return 2, int32(fi.autosize) + int32(sym.Offset)
case "SB":
// Static data reference — needs a relocation (not yet supported).
return -1, offset
default:
return -1, offset
return -1, int32(sym.Offset)
}
}
// riscvITypeLE encodes an I-type instruction and returns little-endian bytes.
func riscvITypeLE(opcode, funct3 uint32, rd, rs1 int, imm int32) []byte {
word := (uint32(imm&0xFFF) << 20) | (uint32(rs1) << 15) |
(funct3 << 12) | (uint32(rd) << 7) | opcode
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
}
// riscvSTypeLE encodes an S-type instruction and returns little-endian bytes.
func riscvSTypeLE(opcode, funct3 uint32, rs1, rs2 int, imm int32) []byte {
immU := uint32(imm) & 0xFFF
word := ((immU >> 5) << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) |
(funct3 << 12) | ((immU & 0x1F) << 7) | opcode
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
return -1, 0
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// TestRISCVFrameSpadjAndLines checks that a framed function records its
// stack-adjustment boundaries and source-line table, the inputs the GOOBJ
// emitter turns into the pcsp/pcfile/pcline tables.
func TestRISCVFrameSpadjAndLines(t *testing.T) {
f, errs := parser.Parse("frame_riscv64.s", `#include "textflag.h"
TEXT ·framed(SB), NOSPLIT, $16-16
MOV a+0(FP), X10
MOV b+8(FP), X11
ADD X11, X10, X10
MOV X10, ret+16(FP)
RET
`)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileRISCV(f)
if err != nil {
t.Fatalf("AssembleFileRISCV: %v", err)
}
fn := img.Funcs[0]
if fn.Size != 24 {
t.Fatalf("size = %d, want 24", fn.Size)
}
// autosize = 16 + 8 = 24; the prologue boundary is just past its C.ADDI
// (SD 4 + C.ADDI 2 = 6), and the RET restores SP just past its C.ADDI
// (RET starts at 16; C.LDSP 2 + C.ADDI 2 = 20).
wantSpadj := []SpadjStep{{PC: 6, Value: 24}, {PC: 20, Value: 0}}
if len(fn.Spadj) != len(wantSpadj) {
t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj)
}
for i := range wantSpadj {
if fn.Spadj[i] != wantSpadj[i] {
t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i])
}
}
// One line entry per instruction, in emission order.
wantLines := []LineEntry{
{Offset: 8, Line: 4},
{Offset: 10, Line: 5},
{Offset: 12, Line: 6},
{Offset: 14, Line: 7},
{Offset: 16, Line: 8},
}
if len(fn.Lines) != len(wantLines) {
t.Fatalf("lines = %v, want %v", fn.Lines, wantLines)
}
for i := range wantLines {
if fn.Lines[i] != wantLines[i] {
t.Errorf("lines[%d] = %v, want %v", i, fn.Lines[i], wantLines[i])
}
}
}
// TestRISCVRegAliases checks the Go ABI register aliases that the toolchain
// defines: LR is the link register (X1) and TMP is the assembler scratch
// register (X31/T6).
func TestRISCVRegAliases(t *testing.T) {
for name, want := range map[string]int{
"X1": 1, "RA": 1, "LR": 1,
"X31": 31, "T6": 31, "TMP": 31,
"X2": 2, "SP": 2,
} {
if got := riscvRegNum(name); got != want {
t.Errorf("riscvRegNum(%q) = %d, want %d", name, got, want)
}
}
}
+5 -4
View File
@@ -40,8 +40,8 @@ DATA answer<>+0(SB)/8, $42
t.Fatalf("AssembleFileRISCV: %v", err)
}
fn := img.Funcs[0]
if fn.Size != 26 {
t.Fatalf("function size = %d, want 26", fn.Size)
if fn.Size != 28 {
t.Fatalf("function size = %d, want 28", fn.Size)
}
if len(fn.Relocs) != 3 {
t.Fatalf("relocs = %d, want 3", len(fn.Relocs))
@@ -95,14 +95,15 @@ DATA answer<>+0(SB)/8, $42
}
// The function code: three AUIPC+second-instruction pairs with zero
// immediates, then the compressed return.
// immediates, then the uncompressed JALR X0, 0(X1) the toolchain emits
// for RET.
code := img.Code[fn.Offset : fn.Offset+fn.Size]
want := append(wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 10, 0)), wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 10, 10, 0))...)
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 11, 0))...)
want = append(want, wordLE(riscvIType(riscvEnc{0x03, 0x3, 0x00}, 11, 11, 0))...)
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 31, 0))...)
want = append(want, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 31, 12, 0))...)
want = append(want, 0x82, 0x80) // C.JR ra
want = append(want, 0x67, 0x80, 0x00, 0x00) // JALR X0, 0(X1)
if !bytes.Equal(code, want) {
t.Errorf("code = % x\nwant % x", code, want)
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
TEXT ·framed(SB), NOSPLIT, $16-16
MOV a+0(FP), X10
MOV b+8(FP), X11
ADD X11, X10, X10
MOV X10, ret+16(FP)
RET
TEXT ·leaf(SB), NOSPLIT, $0-16
MOV a+0(FP), X10
MOV b+8(FP), X11
ADD X11, X10, X10
MOV X10, ret+16(FP)
RET
+57
View File
@@ -0,0 +1,57 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"os"
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// TestGroundTruthRISCV assembles the riscv64 test kernels with gasm and
// compares them byte-for-byte against `go tool asm` (GOARCH=riscv64). The
// relocation fields of static-symbol references are masked before the
// comparison, since the toolchain leaves them zero for the linker.
func TestGroundTruthRISCV(t *testing.T) {
path := "../testdata/verify/basic_riscv64.s"
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
f, errs := parser.Parse(path, string(src))
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := asm.AssembleFileRISCV(f)
if err != nil {
t.Fatalf("AssembleFileRISCV: %v", err)
}
gt, err := GroundTruthRISCV(path)
if err != nil {
t.Fatalf("GroundTruthRISCV: %v", err)
}
matched := 0
for _, fn := range img.Funcs {
gasmCode := maskRelocs(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
goCode, ok := gt[fn.Name]
if !ok {
t.Errorf("%s: not in ground truth (%d functions)", fn.Name, len(gt))
continue
}
goCode = maskRelocs(goCode, fn.Relocs)
if !bytes.Equal(gasmCode, goCode) {
t.Errorf("%s: MISMATCH gasm=%d go=%d bytes\n%s", fn.Name, len(gasmCode), len(goCode), diffHex(gasmCode, goCode))
continue
}
matched++
t.Logf("%s: MATCH (%d bytes)", fn.Name, fn.Size)
}
if matched == 0 {
t.Fatal("no functions matched")
}
}