feat(asm): emit the loong64 stack-split guard for small and medium frames

This commit is contained in:
2026-09-14 21:21:58 +02:00
parent 2e2c0b82a0
commit db50b98179
5 changed files with 183 additions and 8 deletions
+9 -3
View File
@@ -25,11 +25,16 @@ func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
} }
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) { return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
// A pcalau12i+addi.d pair: the high part carries // A pcalau12i+addi.d pair: the high part carries
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO. // R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO; the guard's
if r.Kind == RelLoong64AddrLo { // morestack call carries R_CALLLOONG64.
switch {
case r.Kind == RelLoong64AddrLo:
return relocLoong64AddrLo, 4 return relocLoong64AddrLo, 4
} case r.Kind == RelLoong64Branch:
return relocCallLoong64, 4
default:
return relocLoong64AddrHi, 4 return relocLoong64AddrHi, 4
}
}) })
} }
@@ -39,6 +44,7 @@ func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
const ( const (
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
relocCallLoong64 = 84 // R_CALLLOONG64
) )
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header // toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
+39
View File
@@ -185,3 +185,42 @@ func TestStackGuardBytesRISCV64(t *testing.T) {
} }
} }
} }
// The loong64 stack-split guard, pinned from `go tool asm` (Go 1.27,
// loong64) for the small and medium frame classes plus auto-NOSPLIT. The
// big class (>StackBig) and the huge-frame body prologue remain toolchain
// divergences tracked separately.
func TestStackGuardBytesLOONG64(t *testing.T) {
for _, tt := range []struct {
name string
src string
want string
}{
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
"61a0ff2963a0ff026100c0296360c0022000004c"},
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
"d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00100000000000ffd7ff53"},
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
"61a0ff2963a0ff026100c0296360c0022000004c"},
} {
f, errs := parser.Parse("g_loong64.s", tt.src)
if len(errs) > 0 {
t.Fatalf("%s: parse: %v", tt.name, errs)
}
img, err := AssembleFileLOONG64(f)
if err != nil {
t.Fatalf("%s: assemble: %v", tt.name, err)
}
fn := img.Funcs[0]
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
for _, r := range fn.Relocs {
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
code[j] = 0
}
}
got := hex.EncodeToString(code)
if got != tt.want {
t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want)
}
}
}
+1
View File
@@ -102,6 +102,7 @@ const (
RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair) RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair)
RelArm64Branch // R_CALLARM64 (BL instruction) RelArm64Branch // R_CALLARM64 (BL instruction)
RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair) RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair)
RelLoong64Branch // R_CALLLOONG64 (BL instruction)
) )
type Reloc struct { type Reloc struct {
+28 -5
View File
@@ -27,6 +27,7 @@ import (
func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) { func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := loong64ComputeFrame(t) fi := loong64ComputeFrame(t)
prologue := loong64Prologue(fi) prologue := loong64Prologue(fi)
guardLen := loong64GuardLen(fi)
chain := loong64JumpChain(t) chain := loong64JumpChain(t)
resolve := func(name string) string { resolve := func(name string) string {
if r, ok := chain[name]; ok { if r, ok := chain[name]; ok {
@@ -42,12 +43,12 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
// delta by autosize; the boundary is reported at the third instruction's // delta by autosize; the boundary is reported at the third instruction's
// pc, exactly as the toolchain's pctospadj does. // pc, exactly as the toolchain's pctospadj does.
if fi.autosize != 0 { if fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: 8, Value: fi.autosize}) spadj = append(spadj, SpadjStep{PC: guardLen + 8, Value: fi.autosize})
} }
// Pass 1: label offsets from the instruction sizes. // Pass 1: label offsets from the instruction sizes.
offsets := map[string]int{} offsets := map[string]int{}
pos := len(prologue) pos := guardLen + len(prologue)
for _, stmt := range t.Body { for _, stmt := range t.Body {
switch s := stmt.(type) { switch s := stmt.(type) {
case *ast.Label: case *ast.Label:
@@ -57,9 +58,25 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
} }
} }
// Pass 2: encode. Relocation offsets are recorded function-relative. // Pass 2: encode. The guard prefix precedes the prologue; its branches
out := append([]byte(nil), prologue...) // target the morestack block at the end of the function, which the first
pc := len(prologue) // pass has sized.
bodyLen := 0
{
p := guardLen + len(prologue)
for _, stmt := range t.Body {
if in, ok := stmt.(*ast.Instr); ok {
p += loong64InstrSize(in, fi)
}
}
bodyLen = p - (guardLen + len(prologue))
}
var out []byte
if fi.needSplit {
out = append(out, loong64GuardBytes(fi, guardLen+len(prologue)+bodyLen)...)
}
out = append(out, prologue...)
pc := guardLen + len(prologue)
preCount := len(relocs) preCount := len(relocs)
var lines []LineEntry var lines []LineEntry
for _, stmt := range t.Body { for _, stmt := range t.Body {
@@ -94,6 +111,12 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
out = append(out, code...) out = append(out, code...)
pc += len(code) pc += len(code)
} }
if fi.needSplit {
block, blReloc := loong64MoreStackBlock(pc)
out = append(out, block...)
relocs = append(relocs, blReloc)
pc += len(block)
}
return out, offsets, relocs, lines, spadj, nil return out, offsets, relocs, lines, spadj, nil
} }
+106
View File
@@ -36,6 +36,11 @@ type loong64FrameInfo struct {
args int // the declared -argsize args int // the declared -argsize
noSplit bool // the NOSPLIT flag noSplit bool // the NOSPLIT flag
leaf bool // no call instructions in the body leaf bool // no call instructions in the body
// Stack-split guard state: like amd64 and arm64, a leaf function with a
// small autosize is auto-marked NOSPLIT by the toolchain.
needSplit bool
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
} }
// loong64ComputeFrame derives the frame layout for a TEXT function. // loong64ComputeFrame derives the frame layout for a TEXT function.
@@ -59,9 +64,110 @@ func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
// A zero-frame non-leaf function still opens an 8-byte frame for LR. // A zero-frame non-leaf function still opens an 8-byte frame for LR.
fi.autosize = 8 fi.autosize = 8
} }
switch {
case fi.noSplit:
case fi.autosize < stackSmall && fi.leaf:
// Auto-NOSPLIT, as the toolchain's leaf mark concludes.
default:
fi.needSplit = true
switch {
case fi.autosize <= stackSmall:
fi.splitClass = 0
case fi.autosize <= stackBig:
fi.splitClass = 1
default:
fi.splitClass = 2
}
}
return fi return fi
} }
// loong64GuardLen returns the byte length of the stack-split guard prefix
// (zero when the function needs no guard). The big class materialises two
// constants through R30.
func loong64GuardLen(fi loong64FrameInfo) int {
if !fi.needSplit {
return 0
}
switch fi.splitClass {
case 0:
return 12
case 1:
return 16
default:
return 40 // MOVV + [LU12IW+ORI] + SGTU + BNE + [LU12IW+ORI] + ADDV + SGTU + BEQ
}
}
// loong64Lu12iOri materialises the 32-bit constant v in rd with the
// toolchain's LU12IW/ORI pair (the ORI reads and writes rd itself).
func loong64Lu12iOri(rd int, v int64) []uint32 {
hi := int32(v >> 12)
lo := int32(v & 0xFFF)
return []uint32{
0x0a<<25 | uint32(hi&0xFFFFF)<<5 | uint32(rd),
0x0e<<22 | uint32(lo)<<10 | uint32(rd)<<5 | uint32(rd),
}
}
// loong64GuardBytes emits the stack-split guard prefix. blockStart is the
// function-relative address of the morestack call at the end of the function;
// branch displacements are in instructions.
func loong64GuardBytes(fi loong64FrameInfo, blockStart int) []byte {
// MOVV 16(g), R20 (g.stackguard0), g = R22.
ws := []uint32{l64irr(l64loadStoreTable["MOVV"].ld, 16, 22, 20)}
switch fi.splitClass {
case 0:
// SGTU SP, R20, R20; BEQ R20, more
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 3, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-8)>>2)))
case 1:
off := int32(fi.autosize - stackSmall)
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, int(-off), 3, 24))
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-12)>>2)))
default:
off := int64(fi.autosize - stackSmall)
movLen := 8 // LU12IW + ORI
ws = append(ws, loong64Lu12iOri(30, off)...)
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 30, 3, 24))
ws = append(ws, loong64Bnez(24, int32((blockStart-(8+movLen))>>2)))
ws = append(ws, loong64Lu12iOri(30, -off)...)
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24))
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-loong64GuardLen(fi)+12)>>2)))
}
return l64WordsLE(ws...)
}
// loong64Beqz/loong64Bnez build the 21-bit conditional branches against R0
// that the toolchain emits for its guard compares.
func loong64Beqz(rj int, dispInstr int32) uint32 {
return l64ir21(l64branch21Table["BEQZ"], int(dispInstr), rj)
}
func loong64Bnez(rj int, dispInstr int32) uint32 {
return l64ir21(l64branch21Table["BNEZ"], int(dispInstr), rj)
}
// loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR),
// BL runtime.morestack_noctxt, B back to the function entry.
func loong64MoreStackBlock(blockStart int) ([]byte, Reloc) {
ws := []uint32{
l64rrr(l64DualTable["ADD"].rrr, 0, 1, 31), // MOVV R1, R31 (ADD R1, R0, R31)
l64bbl(l64jumpTable["BL"], 0), // BL, patched by the linker
}
disp := (-(blockStart + 8)) >> 2
ws = append(ws, l64bbl(l64jumpTable["B"], int(disp)))
reloc := Reloc{
Off: blockStart + 4,
After: blockStart + 8,
Name: "runtime\u00b7morestack_noctxt",
Kind: RelLoong64Branch,
}
return l64WordsLE(ws...), reloc
}
// loong64IsLeaf reports whether a function contains no call instructions // loong64IsLeaf reports whether a function contains no call instructions
// (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame // (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame
// and the epilogue shape. // and the epilogue shape.