fix(asm): correct RISC-V frame layout and RVC encodings
Assisted-by: GLM 5.2
This commit is contained in:
+142
-84
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user