fix(asm): encode RISC-V CALL sym(SB) as JAL
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+21
-29
@@ -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)
|
|
||||||
// AUIPC X1, upper 20 bits
|
|
||||||
hi := (offset + 0x800) >> 12
|
|
||||||
word1 := riscvUType(riscvEnc{0x17, 0x0, 0x00}, 1, hi<<12)
|
|
||||||
// JALR X1, lower 12 bits(X1)
|
|
||||||
lo := offset - (hi << 12)
|
|
||||||
word2 := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, 1, lo)
|
|
||||||
var out []byte
|
|
||||||
out = append(out, byte(word1), byte(word1>>8), byte(word1>>16), byte(word1>>24))
|
|
||||||
out = append(out, byte(word2), byte(word2>>8), byte(word2>>16), byte(word2>>24))
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
// CALL with no target: encode as NOP (unsupported).
|
op := ops[0]
|
||||||
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 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
|
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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+8
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user