From 76f8ba6403f682701f6402f346e42cc08161bcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 20:29:44 +0200 Subject: [PATCH] feat(asm): give riscv64 the END and GETCALLERPC the toolchain accepts Assisted-by: GLM 5.3 Flash --- asm/riscv_assemble.go | 101 +++++++++++++++++++++++++++++++++++- asm/riscv_frame.go | 15 ++++-- asm/riscv_toolchain_test.go | 85 ++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 6 deletions(-) diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 10ccce8..54dec9a 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -380,10 +380,14 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo, tlsSyms map[string]bool mnem := instr.Mnemonic.Text ops := instr.Operands mnem = riscvNormalisePseudo(mnem) - if mnem == "FUNCDATA" || mnem == "PCDATA" { - // The bookkeeping statements contribute no bytes. + if mnem == "FUNCDATA" || mnem == "PCDATA" || mnem == "END" { + // The bookkeeping statements and the function-end marker contribute + // no bytes. return 0 } + if mnem == "GETCALLERPC" { + return riscvGetCallerPCSize(ops, fi) + } var immNeg bool mnem, immNeg = riscvNormaliseImmAlias(mnem, ops) if mnem == "RET" { @@ -1136,6 +1140,12 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv return nil, fmt.Errorf("PCDATA expects $n, $m") } return nil, nil + case "END": + // The function-end marker: the toolchain accepts it anywhere in a + // body, ignores whatever operands follow it, and emits nothing. + return nil, nil + case "GETCALLERPC": + return encodeRISCVGetCallerPC(ops, fi, relocs) case "WORD": // WORD $w lays down a raw 32-bit little-endian word, in the range // [0, 0xffffffff] exactly as the toolchain's validation bounds it. @@ -2429,6 +2439,65 @@ func riscvFrameMemSize(op *ast.Operand, fi riscvFrameInfo) int { return len(riscvAddressInX31WithBase(off, rs1)) + 4 } +// encodeRISCVGetCallerPC encodes the toolchain's GETCALLERPC rewrite: on +// entry the caller's address sits in the link register, so a leaf reads it +// straight from X1 (MOV X1, rd, which the compress pass turns into C.MV); +// a body that calls out has clobbered X1, and reads the prologue's save at +// 0(SP) instead (LD rd, 0(SP), compressed to C.LDSP). A memory destination +// stores the address there through the MOV store paths, which the leaf form +// shares with every width and offset; the framed form is the toolchain's +// own rejection, its rewrite reading (SP) into a MOV with two memory ends. +func encodeRISCVGetCallerPC(ops []*ast.Operand, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) { + if len(ops) == 0 { + return nil, fmt.Errorf("GETCALLERPC: unsupported MOV") + } + dst := ops[len(ops)-1] + if isMemOperand(dst) { + if !fi.leaf { + return nil, fmt.Errorf("GETCALLERPC: unsupported MOV") + } + if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" { + // A static destination rides the external store path, exactly + // as MOV X1, sym(SB) does. + return encodeRISCVSBStore(dst.Addr.Sym, 1, relocs), nil + } + if err := riscvWantMemBase("GETCALLERPC", "rs1", dst); err != nil { + return nil, err + } + rs1, off, err := riscvAccessMem("GETCALLERPC", dst, fi) + if err != nil { + return nil, err + } + return riscvFrameMemOp(riscvEnc{0x23, 0x3, 0x00}, true, 1, rs1, off), nil + } + n, bank := riscvBankedRegNum(operandRegName(dst)) + if bank != riscvBankInt { + return nil, fmt.Errorf("GETCALLERPC: expected integer register in rd position but got non-integer register %s", operandRegName(dst)) + } + if fi.leaf { + return wordLE(riscvIType(riscvInstrTable["ADDI"], n, 1, 0)), nil + } + return wordLE(riscvIType(riscvInstrTable["LD"], n, 2, 0)), nil +} + +// riscvGetCallerPCSize returns the encoded size of a GETCALLERPC for the +// layout pass: one word for the register reads, the store's own size +// (expanded where the offset leaves imm12) for a memory destination. +func riscvGetCallerPCSize(ops []*ast.Operand, fi riscvFrameInfo) int { + if len(ops) == 0 { + return 4 + } + dst := ops[len(ops)-1] + if isMemOperand(dst) && (dst.Addr.Sym == nil || dst.Addr.Sym.Pseudo != "SB") { + if err := riscvWantMemOffset("GETCALLERPC", dst, fi); err == nil { + if rs1, off := memFromOperandWithFrame(dst, fi); rs1 >= 0 && !fits12(off) { + return len(riscvAddressInX31WithBase(off, rs1)) + 4 + } + } + } + return 4 +} + // riscvMemInstrSize returns the encoded size of a plain load or store // (integer and FP widths, one register end and one memory end) for the layout // pass: 4 bytes when the offset fits the signed 12-bit span, otherwise the @@ -3124,6 +3193,34 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) { return rvcCS(0x7, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true } + case "GETCALLERPC": + // The rewrite lands on MOV X1, rd (leaf), LD rd, 0(SP) (framed) or + // SD X1, off(base) (leaf, memory destination); each compresses the + // way its own shape does, the toolchain's compress() rules verbatim. + if len(ops) == 0 { + return 0, false + } + dst := ops[len(ops)-1] + if !isMemOperand(dst) { + rd := regFromOperand(dst) + if fi.leaf { + if rd > 0 { + return rvcCR(0x8, uint32(rd), 1), true // C.MV rd, X1 + } + return 0, false + } + // LD rd, 0(SP): the SP form takes every destination but X0. + if rd > 0 { + return rvcLSP(0x3, uint32(rd), 0), true // C.LDSP rd, 0(SP) + } + return 0, false + } + if fi.leaf { + if rs1, off := memFromOperandWithFrame(dst, fi); rs1 == 2 && off >= 0 && off < 512 && off%8 == 0 { + return rvcSSP(0x7, 1, uint32(off)), true // C.SDSP X1, off + } + } + case "LW": rd, rs1, imm := extractLDParams(instr, fi) if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 256 && imm%4 == 0 { diff --git a/asm/riscv_frame.go b/asm/riscv_frame.go index c7e0202..2ac9a93 100644 --- a/asm/riscv_frame.go +++ b/asm/riscv_frame.go @@ -34,6 +34,11 @@ import ( type riscvFrameInfo struct { autosize int // the real SP adjustment (locals + saved LR) + // leaf mirrors the toolchain's cursym.Leaf: the body makes no call, so + // the link register still holds the caller's address. GETCALLERPC reads + // it there; a framed body reads the prologue's save at 0(SP) instead. + leaf bool + // Stack-split guard state: the toolchain emits the check for every // non-NOSPLIT function whose autosize is nonzero (a zero autosize is // "effectively NOSPLIT"); unlike amd64 and arm64 there is no leaf @@ -44,12 +49,12 @@ type riscvFrameInfo struct { // riscvComputeFrame derives the frame layout for a TEXT function. func riscvComputeFrame(t *ast.Text) riscvFrameInfo { - frame := frameSize(t) - if frame != 0 || !riscvIsLeaf(t) { + leaf := riscvIsLeaf(t) + if frame := frameSize(t); frame != 0 || !leaf { // FixedFrameSize = 8: space for the saved link register. A // zero-frame non-leaf function still opens an 8-byte frame for LR. autosize := frame + 8 - fi := riscvFrameInfo{autosize: autosize} + fi := riscvFrameInfo{autosize: autosize, leaf: leaf} if !hasNoSplitFlag(t) { fi.needSplit = true switch { @@ -63,7 +68,9 @@ func riscvComputeFrame(t *ast.Text) riscvFrameInfo { } return fi } - return riscvFrameInfo{} + // A zero-frame leaf: no prologue, no guard, and the link register still + // holding the caller's address. + return riscvFrameInfo{leaf: leaf} } // hasNoSplitFlag reports whether the TEXT directive carries NOSPLIT. diff --git a/asm/riscv_toolchain_test.go b/asm/riscv_toolchain_test.go index 5f9f85c..252293f 100644 --- a/asm/riscv_toolchain_test.go +++ b/asm/riscv_toolchain_test.go @@ -241,6 +241,91 @@ func TestRISCVToolchainRejections(t *testing.T) { } } +// TestRISCVGetCallerPC_Differential proves the toolchain's GETCALLERPC +// rewrite: a leaf reads the caller's address straight from the link register +// (MOV X1, rd, compressed to C.MV; X0 stays the discarded ADDI), a body that +// calls out reads the prologue's save at 0(SP) (LD rd, 0(SP), compressed to +// C.LDSP, the reserved SP destination included; X0 stays uncompressed), and a +// memory destination stores it there, through the frame mapping and the +// static-data path alike. +func TestRISCVGetCallerPC_Differential(t *testing.T) { + leaf := `#include "textflag.h" + +TEXT ·leafgcp(SB), NOSPLIT, $0 + GETCALLERPC X5 // c.mv X5, X1 + GETCALLERPC X0 // addi X0, X1, 0, discarded + GETCALLERPC 8(X6) // sd X1, 8(X6) + GETCALLERPC x+8(FP) // c.sdsp X1, 16 + END + RET +` + path := writeRISCVSrc(t, "gcp_leaf_riscv64.s", leaf) + assertRISCVDifferential(t, path, leaf, "leafgcp") + + framed := `#include "textflag.h" + +TEXT ·framegcp(SB), $8 + CALL extcall(SB) + GETCALLERPC X5 // c.ldsp X5, 0(SP) + GETCALLERPC X2 // c.ldsp X2, 0(SP) + GETCALLERPC X0 // ld X0, 0(SP), uncompressed + END + RET +` + path = writeRISCVSrc(t, "gcp_frame_riscv64.s", framed) + assertRISCVDifferential(t, path, framed, "framegcp") +} + +// TestRISCVGetCallerPCRejections pins the rewrite's rejections: a framed +// body with a memory destination would read (SP) into a MOV with two memory +// ends, a non-integer register is the rd bank rejection, and no destination +// at all is the toolchain's unsupported MOV. +func TestRISCVGetCallerPCRejections(t *testing.T) { + cases := []struct { + name string + src string + want string + }{ + { + name: "memory destination in a framed body", + src: "CALL extcall(SB)\n\tGETCALLERPC 8(X6)\n", + want: "GETCALLERPC: unsupported MOV", + }, + { + name: "float destination", + src: "\tGETCALLERPC F5\n", + want: "GETCALLERPC: expected integer register in rd position but got non-integer register F5", + }, + { + name: "vector destination", + src: "\tGETCALLERPC V1\n", + want: "GETCALLERPC: expected integer register in rd position but got non-integer register V1", + }, + { + name: "immediate destination", + src: "\tGETCALLERPC $5\n", + want: "GETCALLERPC: expected integer register in rd position", + }, + { + name: "no destination", + src: "\tGETCALLERPC\n", + want: "GETCALLERPC: unsupported MOV", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + fn := firstTextRISCV(t, "#include \"textflag.h\"\n\nTEXT ·r(SB), NOSPLIT, $0\n"+tc.src+"\tRET\n") + _, _, _, _, _, _, err := assembleRISCV(fn, nil) + if err == nil { + t.Fatalf("source assembled, want rejection %q", tc.want) + } + if !strings.Contains(err.Error(), tc.want) { + t.Errorf("error %q does not carry %q", err.Error(), tc.want) + } + }) + } +} + // TestRISCVTLSRelocKind pins the relocation the TLS sequence records: one // R_RISCV_TLS_LE field spanning the LUI + ADDIW pair, the symbol named and // no addend for a plain tls(SB) reference.