diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1ddad..d4227d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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); diff --git a/asm/elfriscv.go b/asm/elfriscv.go index 7378d0c..eed81ec 100644 --- a/asm/elfriscv.go +++ b/asm/elfriscv.go @@ -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}) diff --git a/asm/link.go b/asm/link.go index 0bafa1d..0bb226b 100644 --- a/asm/link.go +++ b/asm/link.go @@ -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 { diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index ebd2eb6..143d2df 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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 } } } diff --git a/asm/riscv_encode.go b/asm/riscv_encode.go index f987104..6df678a 100644 --- a/asm/riscv_encode.go +++ b/asm/riscv_encode.go @@ -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 diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index aa19a33..ecd3c23 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -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)) } } diff --git a/asm/riscv_frame.go b/asm/riscv_frame.go index ce50e80..6d26c6e 100644 --- a/asm/riscv_frame.go +++ b/asm/riscv_frame.go @@ -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 } diff --git a/asm/riscv_frame_test.go b/asm/riscv_frame_test.go new file mode 100644 index 0000000..e0f5e20 --- /dev/null +++ b/asm/riscv_frame_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Petr Balvín (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) + } + } +} diff --git a/asm/riscv_goobj_test.go b/asm/riscv_goobj_test.go index 8277936..d3202f4 100644 --- a/asm/riscv_goobj_test.go +++ b/asm/riscv_goobj_test.go @@ -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) } diff --git a/testdata/verify/basic_riscv64.s b/testdata/verify/basic_riscv64.s new file mode 100644 index 0000000..a0672f6 --- /dev/null +++ b/testdata/verify/basic_riscv64.s @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Petr Balvín (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 diff --git a/verify/riscv_groundtruth_test.go b/verify/riscv_groundtruth_test.go new file mode 100644 index 0000000..4e0331b --- /dev/null +++ b/verify/riscv_groundtruth_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Petr Balvín (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") + } +}