fix(asm): encode RISC-V CALL sym(SB) as JAL
Test / vet (push) Successful in 47s
Test / test (push) Failing after 2m9s
Test / build (push) Skipped

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-13 18:12:22 +02:00
parent 681a449c01
commit c05c53452f
9 changed files with 169 additions and 44 deletions
+8
View File
@@ -82,6 +82,14 @@ Unreleased changes on the `development` branch.
encoders were removed, and the `C.LUI` direct-instruction compression now encoders were removed, and the `C.LUI` direct-instruction compression now
uses the correct six-bit signed range. A byte-exact ground-truth test 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`. 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 - **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
hardware watchpoint slot 0, so a second `watch` call silently overwrote hardware watchpoint slot 0, so a second `watch` call silently overwrote
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3); the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
+3
View File
@@ -15,6 +15,7 @@ const (
// RISC-V relocation types. // RISC-V relocation types.
rRISCV32 = 1 rRISCV32 = 1
rRISCVJAL = 17 // R_RISCV_JAL
rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20 rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20
rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I
rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S 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), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: 0}, 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: case RelPCRelAbs:
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend}) relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend})
default: default:
+8 -3
View File
@@ -25,17 +25,22 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
return nil, err return nil, err
} }
return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) { return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) {
if r.Kind == RelRISCVPCRELSType { switch r.Kind {
case RelRISCVPCRELSType:
return relocRISCVPcrelStype, 8 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 // 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 // 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 ( const (
relocRISCVJal = 59 // R_RISCV_JAL
relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE
relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE
) )
+1
View File
@@ -92,6 +92,7 @@ const (
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64) RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair) RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-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) RelPCRelAbs // 32-bit absolute (R_RISCV_32)
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
+20 -28
View File
@@ -100,13 +100,14 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
if c16, ok := tryCompressRVC(r.instr, fi); ok { if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)} code = []byte{byte(c16), byte(c16 >> 8)}
} }
// Make newly added relocation offsets absolute (subtract prologue to make // Make newly added relocation offsets function-relative. Each
// them function-relative, then the caller adds fn.Offset). After // instruction records its reloc offset relative to its own start;
// points just past the AUIPC+second-instruction pair, which is // the current pc is that instruction's offset from the function
// always 8 bytes wide for these static-symbol references. // 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++ { for j := preCount; j < len(relocs); j++ {
relocs[j].Off += pc - len(prologue) relocs[j].Off += pc
relocs[j].After = relocs[j].Off + 8 relocs[j].After += pc
} }
preCount = len(relocs) preCount = len(relocs)
// The RET's epilogue closes the frame: the SP delta returns to zero // 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. // Handle pseudo-instructions and special cases first.
switch mnem { switch mnem {
case "RET": 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 return riscvReturn(fi), nil
case "CALL": case "CALL":
// CALL target → AUIPC X1, %pcrel_hi + JALR X1, %pcrel_lo(X1). // CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL
// For now, emit AUIPC X1, 0 + JALR X1, 0(X1) with zero offsets. // relocation. The Go assembler rejects CALL to a local branch label.
// The relocation system will fill the actual offsets. if len(ops) != 1 {
if len(ops) >= 1 { return nil, fmt.Errorf("CALL expects 1 operand, got %d", len(ops))
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) op := ops[0]
// AUIPC X1, upper 20 bits if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
hi := (offset + 0x800) >> 12 return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
word1 := riscvUType(riscvEnc{0x17, 0x0, 0x00}, 1, hi<<12)
// JALR X1, lower 12 bits(X1)
lo := offset - (hi << 12)
word2 := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, 1, lo)
var out []byte
out = append(out, byte(word1), byte(word1>>8), byte(word1>>16), byte(word1>>24))
out = append(out, byte(word2), byte(word2>>8), byte(word2>>16), byte(word2>>24))
return out, nil
} }
// CALL with no target: encode as NOP (unsupported). if relocs != nil {
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0) *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 return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
case "JMP": case "JMP":
// JMP = JAL X0, target. The Go assembler never compresses this to // JMP = JAL X0, target. The Go assembler never compresses this to
+33 -8
View File
@@ -4,6 +4,7 @@
package asm package asm
import ( import (
"bytes"
"testing" "testing"
"sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/ast"
@@ -720,19 +721,43 @@ TEXT ·badfp(SB), NOSPLIT, $0
} }
func TestRISCV_CALL(t *testing.T) { 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" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·calltest(SB), NOSPLIT, $0 TEXT ·calltest(SB), NOSPLIT, $0
CALL sub CALL ext(SB)
done:
RET 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: sub:
RET RET
`) `)
code := assembleRISCVHelper(t, fn) _, _, _, _, _, err := assembleRISCV(fn)
// A zero-frame non-leaf function saves LR: prologue (8) + CALL (8) + if err == nil {
// two framed RETs (8 each) = 32. t.Error("expected error for CALL to local label, got nil")
if len(code) != 32 {
t.Errorf("expected 32 bytes with CALL, got %d", len(code))
} }
} }
+86 -4
View File
@@ -5,6 +5,7 @@ package asm
import ( import (
"bytes" "bytes"
"debug/elf"
"encoding/binary" "encoding/binary"
"os" "os"
"os/exec" "os/exec"
@@ -15,10 +16,64 @@ import (
"sourcedock.dev/petrbalvin/gasm-devkit/parser" "sourcedock.dev/petrbalvin/gasm-devkit/parser"
) )
// TestGOObjectRISCVStructure checks the emitted RISC-V object's relocations: // TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL
// each AUIPC + second-instruction pair carries a single 8-byte // instruction carrying an R_RISCV_JAL relocation (4-byte field) in both the
// R_RISCV_PCREL_ITYPE/STYPE relocation, exactly as `go tool asm` models it // GOOBJ and ELF object emitters.
// (rather than the ELF HI20/LO12 pair). 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) { func TestGOObjectRISCVStructure(t *testing.T) {
f, errs := parser.Parse("k_riscv64.s", ` f, errs := parser.Parse("k_riscv64.s", `
#include "textflag.h" #include "textflag.h"
@@ -262,3 +317,30 @@ func main() {
t.Errorf("main.add not found in linked binary:\n%s", nmOut) 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
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
TEXT ·call(SB), NOSPLIT, $0
CALL callee(SB)
RET
+1
View File
@@ -24,6 +24,7 @@ func TestGroundTruthRISCV(t *testing.T) {
"../testdata/verify/largeimm_riscv64.s", "../testdata/verify/largeimm_riscv64.s",
"../testdata/verify/movimm_riscv64.s", "../testdata/verify/movimm_riscv64.s",
"../testdata/verify/branch_riscv64.s", "../testdata/verify/branch_riscv64.s",
"../testdata/verify/call_riscv64.s",
} { } {
t.Run(path, func(t *testing.T) { t.Run(path, func(t *testing.T) {
testGroundTruthRISCVFile(t, path) testGroundTruthRISCVFile(t, path)