diff --git a/CHANGELOG.md b/CHANGELOG.md index 9991481..b614510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,14 @@ Unreleased changes on the `development` branch. encoders were removed, and the `C.LUI` direct-instruction compression now uses the correct six-bit signed range. A byte-exact ground-truth test covers the branch family and jumps against `GOARCH=riscv64 go tool asm`. +- **RISC-V `CALL sym(SB)`.** The call pseudo-instruction now emits the + toolchain's `JAL X1, sym(SB)` with a single `R_RISCV_JAL` relocation + (previously it emitted an `AUIPC`+`JALR` pair against a local branch + label, a form `go tool asm` rejects). The GOOBJ and ELF emitters now map + that relocation (Go objabi 59 / ELF `R_RISCV_JAL` 17, a 4-byte field), and + relocation offsets are recorded relative to the function start (including + the prologue). A byte-exact ground-truth test covers a call 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 eed81ec..1f5cf3d 100644 --- a/asm/elfriscv.go +++ b/asm/elfriscv.go @@ -15,6 +15,7 @@ const ( // RISC-V relocation types. rRISCV32 = 1 + rRISCVJAL = 17 // R_RISCV_JAL rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20 rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S @@ -109,6 +110,8 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { 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: 0}, ) + case RelRISCVJal: + relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, sym: idx, addend: r.Addend}) case RelPCRelAbs: relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend}) default: diff --git a/asm/goobjriscv.go b/asm/goobjriscv.go index a8a98d3..4365c7f 100644 --- a/asm/goobjriscv.go +++ b/asm/goobjriscv.go @@ -25,17 +25,22 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { return nil, err } return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) { - if r.Kind == RelRISCVPCRELSType { + switch r.Kind { + case RelRISCVPCRELSType: return relocRISCVPcrelStype, 8 + case RelRISCVJal: + return relocRISCVJal, 4 + default: + return relocRISCVPcrelItype, 8 } - return relocRISCVPcrelItype, 8 }) } // RISC-V relocation types (cmd/internal/objabi). The Go linker applies // R_RISCV_PCREL_ITYPE/STYPE to an AUIPC + I/S-type instruction pair as a -// single 8-byte field. +// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction. const ( + relocRISCVJal = 59 // R_RISCV_JAL relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE ) diff --git a/asm/link.go b/asm/link.go index 0bb226b..15101d8 100644 --- a/asm/link.go +++ b/asm/link.go @@ -92,6 +92,7 @@ const ( RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64) RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair) RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair) + RelRISCVJal // R_RISCV_JAL (J-type call) RelPCRelAbs // 32-bit absolute (R_RISCV_32) RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index a73406b..1e738e9 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -100,13 +100,14 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ if c16, ok := tryCompressRVC(r.instr, fi); ok { code = []byte{byte(c16), byte(c16 >> 8)} } - // Make newly added relocation offsets absolute (subtract prologue to make - // them function-relative, then the caller adds fn.Offset). After - // points just past the AUIPC+second-instruction pair, which is - // always 8 bytes wide for these static-symbol references. + // Make newly added relocation offsets function-relative. Each + // instruction records its reloc offset relative to its own start; + // the current pc is that instruction's offset from the function + // start (which includes the prologue). After is the address just + // past the relocated field, shifted by the same amount. for j := preCount; j < len(relocs); j++ { - relocs[j].Off += pc - len(prologue) - relocs[j].After = relocs[j].Off + 8 + relocs[j].Off += pc + relocs[j].After += pc } preCount = len(relocs) // The RET's epilogue closes the frame: the SP delta returns to zero @@ -175,32 +176,23 @@ 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 = epilogue (restore LR and close the frame when present) + C.JR ra. + // RET = epilogue (restore LR and close the frame when present) + + // uncompressed JALR X0, 0(X1) (the toolchain never compresses RET). 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. - // 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%s", target, suggestLabel(target, offsets)) - } - 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 sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL + // relocation. The Go assembler rejects CALL to a local branch label. + if len(ops) != 1 { + return nil, fmt.Errorf("CALL expects 1 operand, got %d", len(ops)) } - // CALL with no target: encode as NOP (unsupported). - word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0) + op := ops[0] + if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" { + return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))") + } + if relocs != nil { + *relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelRISCVJal, Addend: op.Addr.Sym.Offset}) + } + word = riscvJType(1, 0) // JAL X1, 0 — the linker fills the offset return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil case "JMP": // JMP = JAL X0, target. The Go assembler never compresses this to diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 31a4d22..34218a7 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -4,6 +4,7 @@ package asm import ( + "bytes" "testing" "sourcedock.dev/petrbalvin/gasm-devkit/ast" @@ -720,19 +721,43 @@ TEXT ·badfp(SB), NOSPLIT, $0 } func TestRISCV_CALL(t *testing.T) { - // CALL target → AUIPC + JALR (8 bytes). + // CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL relocation. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·calltest(SB), NOSPLIT, $0 - CALL sub -done: + CALL ext(SB) RET +`) + code, _, relocs, _, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + // prologue (8) + JAL (4) + epilogue+JALR (8) = 20 + if len(code) != 20 { + t.Fatalf("expected 20 bytes with CALL sym(SB), got %d", len(code)) + } + if len(relocs) != 1 { + t.Fatalf("relocs = %d, want 1", len(relocs)) + } + r := relocs[0] + if r.Kind != RelRISCVJal || r.Name != "ext" || r.Off != 8 || r.After != 12 || r.Addend != 0 { + t.Errorf("reloc = {kind %v off %d after %d name %q addend %d}", r.Kind, r.Off, r.After, r.Name, r.Addend) + } + // The JAL instruction itself is JAL X1, 0 at function offset 8. + wantJAL := wordLE(riscvJType(1, 0)) + if !bytes.Equal(code[8:12], wantJAL) { + t.Errorf("JAL = % x, want % x", code[8:12], wantJAL) + } +} + +func TestRISCV_CALL_local_error(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·calllocal(SB), NOSPLIT, $0 + CALL sub sub: RET `) - code := assembleRISCVHelper(t, fn) - // 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)) + _, _, _, _, _, err := assembleRISCV(fn) + if err == nil { + t.Error("expected error for CALL to local label, got nil") } } diff --git a/asm/riscv_goobj_test.go b/asm/riscv_goobj_test.go index d3202f4..3a2e6b2 100644 --- a/asm/riscv_goobj_test.go +++ b/asm/riscv_goobj_test.go @@ -5,6 +5,7 @@ package asm import ( "bytes" + "debug/elf" "encoding/binary" "os" "os/exec" @@ -15,10 +16,64 @@ import ( "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) -// TestGOObjectRISCVStructure checks the emitted RISC-V object's relocations: -// each AUIPC + second-instruction pair carries a single 8-byte -// R_RISCV_PCREL_ITYPE/STYPE relocation, exactly as `go tool asm` models it -// (rather than the ELF HI20/LO12 pair). +// TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL +// instruction carrying an R_RISCV_JAL relocation (4-byte field) in both the +// GOOBJ and ELF object emitters. +func TestGOObjectRISCVCallReloc(t *testing.T) { + f, errs := parser.Parse("k_riscv64.s", ` +#include "textflag.h" + +TEXT ·c(SB), NOSPLIT, $0-0 + CALL callee<>(SB) + RET + +GLOBL callee<>(SB), RODATA, $8 +DATA callee<>+0(SB)/8, $42 +`) + 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 len(fn.Relocs) != 1 { + t.Fatalf("relocs = %d, want 1", len(fn.Relocs)) + } + r := fn.Relocs[0] + if r.Kind != RelRISCVJal || r.Off != 8 || r.After != 12 || r.Name != "callee" || r.Addend != 0 || r.External { + t.Errorf("reloc = {kind %v off %d after %d name %q addend %d external %v}", r.Kind, r.Off, r.After, r.Name, r.Addend, r.External) + } + + obj, err := img.GOObjectRISCV("testpkg", "k_riscv64.s") + if err != nil { + t.Fatalf("GOObjectRISCV: %v", err) + } + v := openGoobj(t, obj) + relocIdx := v.blk(blkRelocIdx) + relocs := v.blk(blkReloc) + // The function is the last non-package symbol: 4 package defs, then the + // 4 pc tables and the function. + first := int(binary.LittleEndian.Uint32(relocIdx[(4+4)*4:])) + if (first+1)*23 > len(relocs) { + t.Fatalf("reloc block too short: first=%d len=%d", first, len(relocs)) + } + e := relocs[first*23:] + le := binary.LittleEndian + if int32(le.Uint32(e[0:])) != 8 || e[4] != 4 || le.Uint16(e[5:]) != relocRISCVJal || le.Uint32(e[15:]) != pkgIdxSelf || le.Uint32(e[19:]) != 0 { + t.Errorf("GOOBJ reloc = off %d size %d type %d pkg %d sym %d", int32(le.Uint32(e[0:])), e[4], le.Uint16(e[5:]), le.Uint32(e[15:]), le.Uint32(e[19:])) + } + + // The ELF object must carry a single R_RISCV_JAL relocation in .rela.text. + elfObj, err := img.ELFRISCVObject() + if err != nil { + t.Fatalf("ELFRISCVObject: %v", err) + } + if !hasELFRISCVJAL(t, elfObj) { + t.Error("ELF object missing R_RISCV_JAL relocation") + } +} func TestGOObjectRISCVStructure(t *testing.T) { f, errs := parser.Parse("k_riscv64.s", ` #include "textflag.h" @@ -262,3 +317,30 @@ func main() { t.Errorf("main.add not found in linked binary:\n%s", nmOut) } } + +// hasELFRISCVJAL reports whether the ELF object carries an R_RISCV_JAL +// relocation in its .rela.text section. +func hasELFRISCVJAL(t *testing.T, data []byte) bool { + t.Helper() + f, err := elf.NewFile(bytes.NewReader(data)) + if err != nil { + t.Fatalf("parse ELF: %v", err) + } + defer f.Close() + rela := f.Section(".rela.text") + if rela == nil { + return false + } + b, err := rela.Data() + if err != nil { + t.Fatalf(".rela.text data: %v", err) + } + const rRISCVJAL = 17 + for i := 0; i+24 <= len(b); i += 24 { + info := binary.LittleEndian.Uint64(b[i+8:]) + if uint32(info) == rRISCVJAL { + return true + } + } + return false +} diff --git a/testdata/verify/call_riscv64.s b/testdata/verify/call_riscv64.s new file mode 100644 index 0000000..947841a --- /dev/null +++ b/testdata/verify/call_riscv64.s @@ -0,0 +1,8 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +#include "textflag.h" + +TEXT ·call(SB), NOSPLIT, $0 + CALL callee(SB) + RET diff --git a/verify/riscv_groundtruth_test.go b/verify/riscv_groundtruth_test.go index 7c6b913..300d110 100644 --- a/verify/riscv_groundtruth_test.go +++ b/verify/riscv_groundtruth_test.go @@ -24,6 +24,7 @@ func TestGroundTruthRISCV(t *testing.T) { "../testdata/verify/largeimm_riscv64.s", "../testdata/verify/movimm_riscv64.s", "../testdata/verify/branch_riscv64.s", + "../testdata/verify/call_riscv64.s", } { t.Run(path, func(t *testing.T) { testGroundTruthRISCVFile(t, path)