feat(asm): emit the loong64 stack-split guard for big frames

This commit is contained in:
2026-09-14 22:38:58 +02:00
parent db50b98179
commit 70218e84ba
7 changed files with 294 additions and 67 deletions
+11 -4
View File
@@ -182,7 +182,7 @@ func arm64Prologue(fi arm64FrameInfo) []byte {
// arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value // arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value
// fits the imm12 field, otherwise the toolchain materialises it into REGTMP // fits the imm12 field, otherwise the toolchain materialises it into REGTMP
// (R27) and subtracts the register. // (R27) and subtracts the register in the extended-register form.
func arm64SubImmWords(imm uint32, rd uint32) []uint32 { func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
if imm <= 0xFFF { if imm <= 0xFFF {
return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)} return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)}
@@ -191,7 +191,7 @@ func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
if err != nil { if err != nil {
mov = nil mov = nil
} }
return append(wordsOf(mov), arm64DPSRWords(arm64OpSub, 27, 31, rd)) return append(wordsOf(mov), arm64DPExtWords(arm64OpSub, 27, 31, rd))
} }
// arm64AddImmWords emits ADD $imm, SP, Rd with the same REGTMP fallback. // arm64AddImmWords emits ADD $imm, SP, Rd with the same REGTMP fallback.
@@ -203,7 +203,7 @@ func arm64AddImmWords(imm uint32, rd uint32) []uint32 {
if err != nil { if err != nil {
mov = nil mov = nil
} }
return append(wordsOf(mov), arm64DPSRWords(arm64OpAdd, 27, 31, rd)) return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, rd))
} }
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and // arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
@@ -326,6 +326,13 @@ func arm64DPSRWords(base uint32, rm, rn, rd uint32) uint32 {
return base | rm<<16 | rn<<5 | rd return base | rm<<16 | rn<<5 | rd
} }
// arm64DPExtWords builds one data-processing (extended register) word, the
// form the toolchain picks when a large immediate was materialised into
// REGTMP before the operation: base | 1<<21 | Rm<<16 | UXTX<<13 | Rn<<5 | Rd.
func arm64DPExtWords(base, rm, rn, rd uint32) uint32 {
return base | 1<<21 | rm<<16 | 3<<13 | rn<<5 | rd
}
// wordsOf converts little-endian instruction bytes back to words. // wordsOf converts little-endian instruction bytes back to words.
func wordsOf(b []byte) []uint32 { func wordsOf(b []byte) []uint32 {
ws := make([]uint32, 0, len(b)/4) ws := make([]uint32, 0, len(b)/4)
@@ -359,7 +366,7 @@ func arm64GuardBytes(fi arm64FrameInfo, blockStart int) []byte {
} }
ws = append(ws, wordsOf(mov)...) ws = append(ws, wordsOf(mov)...)
ml := len(mov) / 4 ml := len(mov) / 4
ws = append(ws, arm64DPSRWords(arm64OpSubs, 27, 31, 17)) // SUBS R27, RSP, R17 ws = append(ws, arm64DPExtWords(arm64OpSubs, 27, 31, 17)) // SUBS R17, RSP, R27
ws = append(ws, br(8+ml, a64CondLO)) ws = append(ws, br(8+ml, a64CondLO))
ws = append(ws, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17 ws = append(ws, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17
ws = append(ws, br(8+ml+8, a64CondLS)) ws = append(ws, br(8+ml+8, a64CondLS))
+5 -1
View File
@@ -16,6 +16,7 @@ const (
// LoongArch relocation types (the ELF psABI). // LoongArch relocation types (the ELF psABI).
rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i) rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i)
rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st) rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st)
rLarchB26 = 66 // R_LARCH_B26 (b/bl, matches the Go linker's mapping)
) )
// ELFLOONG64Object returns the image as an ELF64 relocatable object file for // ELFLOONG64Object returns the image as an ELF64 relocatable object file for
@@ -95,8 +96,11 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
} }
typ := uint32(rLarchPCALAHI20) typ := uint32(rLarchPCALAHI20)
if r.Kind == RelLoong64AddrLo { switch r.Kind {
case RelLoong64AddrLo:
typ = rLarchPCALALO12 typ = rLarchPCALALO12
case RelLoong64Branch:
typ = rLarchB26
} }
relas = append(relas, elfRela{ relas = append(relas, elfRela{
off: uint64(fn.Offset + r.Off), off: uint64(fn.Offset + r.Off),
+42
View File
@@ -198,3 +198,45 @@ TEXT ·nop(SB), NOSPLIT, $0
t.Error("function symbol nop not found") t.Error("function symbol nop not found")
} }
} }
// TestELFLOONG64BranchRelocation checks that the morestack call and an
// internal CALL both carry R_LARCH_B26 in the emitted object, matching the
// Go linker's mapping of its call relocation.
func TestELFLOONG64BranchRelocation(t *testing.T) {
f, errs := parser.Parse("k_loong64.s", "TEXT \u00b7callbig(SB), $8192-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileLOONG64(f)
if err != nil {
t.Fatalf("AssembleFileLOONG64: %v", err)
}
obj, err := img.ELFLOONG64Object()
if err != nil {
t.Fatalf("ELFLOONG64Object: %v", err)
}
ef, err := elf.NewFile(bytes.NewReader(obj))
if err != nil {
t.Fatalf("parse emitted object: %v", err)
}
defer ef.Close()
relaSec := ef.Section(".rela.text")
if relaSec == nil {
t.Fatal("missing .rela.text")
}
raw, err := relaSec.Data()
if err != nil {
t.Fatal(err)
}
// The guard's morestack call plus the body's CALL to other.
if len(raw)%24 != 0 || len(raw)/24 != 2 {
t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw))
}
le := binary.LittleEndian
for i := range 2 {
info := le.Uint64(raw[i*24+8:])
if elf.R_LARCH(info&0xffffffff) != elf.R_LARCH_B26 {
t.Errorf("relocation %d type = %v, want R_LARCH_B26", i, elf.R_LARCH(info&0xffffffff))
}
}
}
+11 -1
View File
@@ -156,6 +156,16 @@ const goobjBuiltinMorestackNoctxt = 246
// stack-guard call. // stack-guard call.
var goobjBuiltinMorestack = "runtime\u00b7morestack_noctxt" var goobjBuiltinMorestack = "runtime\u00b7morestack_noctxt"
// isCallReloc reports whether k is one of the per-arch call relocations a
// direct branch to a TEXT symbol carries.
func isCallReloc(k RelocKind) bool {
switch k {
case RelCall, RelRISCVJal, RelArm64Branch, RelLoong64Branch:
return true
}
return false
}
const goobjMagic = "\x00go120ld" const goobjMagic = "\x00go120ld"
// goSym is one symbol definition under construction. // goSym is one symbol definition under construction.
@@ -436,7 +446,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
// A call to a TEXT function of the same file references the // A call to a TEXT function of the same file references the
// non-package definition table. // non-package definition table.
ni, isText := textNpIdx[r.Name] ni, isText := textNpIdx[r.Name]
if !isText || r.Kind != RelCall { if !isText || !isCallReloc(r.Kind) {
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name) return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
} }
pkg = pkgIdxNone pkg = pkgIdxNone
+62 -5
View File
@@ -8,6 +8,7 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/parser" "sourcedock.dev/petrbalvin/gasm-devkit/parser"
) )
@@ -116,7 +117,7 @@ func TestStackGuardBytesARM64(t *testing.T) {
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
"900b40f9f14302d13f0210eb09010054f44304d19dfa3fa99f020091fd2300d1fd230491ff430491c0035fd6e3031eaa00000000f3ffff17"}, "900b40f9f14302d13f0210eb09010054f44304d19dfa3fa99f020091fd2300d1fd230491ff430491c0035fd6e3031eaa00000000f3ffff17"},
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n", {"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
"900b40f91bf283d2f1031beba30100543f0210eb690100541b0284d2f4031bcb9dfa3fa99f020091fd2300d11b0184d2fd031b8b1b0284d2ff031b8bc0035fd6e3031eaa00000000eeffff17"}, "900b40f91bf283d2f1633beba30100543f0210eb690100541b0284d2f4633bcb9dfa3fa99f020091fd2300d11b0184d2fd633b8b1b0284d2ff633b8bc0035fd6e3031eaa00000000eeffff17"},
{"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n", {"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
"900b40f9ff6330eb09010054fe0f1ef8fd831ff8fd2300d100000000fd835ff8fe0742f8c0035fd6e3031eaa00000000f4ffff17"}, "900b40f9ff6330eb09010054fe0f1ef8fd831ff8fd2300d100000000fd835ff8fe0742f8c0035fd6e3031eaa00000000f4ffff17"},
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n", {"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
@@ -187,9 +188,10 @@ func TestStackGuardBytesRISCV64(t *testing.T) {
} }
// The loong64 stack-split guard, pinned from `go tool asm` (Go 1.27, // 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 // loong64): every guard class (including the medium class with the
// big class (>StackBig) and the huge-frame body prologue remain toolchain // materialised constant and the big class with the ORI-less constants), the
// divergences tracked separately. // auto-NOSPLIT leaf behaviour, the large-frame R30 prologue/epilogue forms
// and the morestack block. Relocation fields are masked.
func TestStackGuardBytesLOONG64(t *testing.T) { func TestStackGuardBytesLOONG64(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
@@ -199,9 +201,37 @@ func TestStackGuardBytesLOONG64(t *testing.T) {
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n", {"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
"61a0ff2963a0ff026100c0296360c0022000004c"}, "61a0ff2963a0ff026100c0296360c0022000004c"},
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
"d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00100000000000ffd7ff53"}, "d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00150000000000ffd7ff53"},
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n", {"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
"61a0ff2963a0ff026100c0296360c0022000004c"}, "61a0ff2963a0ff026100c0296360c0022000004c"},
// The LR store leaves the 12-bit store-offset range while the SP
// adjust immediate still fits, and the epilogue adjusts through a
// single ORI.
{"fit2048", "TEXT \u00b7fit2048(SB), $2040-0\n\tRET\n",
"d442c0287800e20294e21200802600401e000014de8f1000c103e0296300e0026100c0291e00a00363f810002000004c3f00150000000000ffcbff53"},
// Medium class at the materialisation boundary (off = 2048 still
// immediate, 2049+ goes through R30).
{"med2048off", "TEXT \u00b7med2048off(SB), $2168-0\n\tRET\n",
"d442c0287800e00294e21200802e0040feffff15de8f1000c103de29feffff15de039e0363f810006100c0291e00a20363f810002000004c3f00150000000000ffc3ff53"},
{"medmat", "TEXT \u00b7medmat(SB), $2176-0\n\tRET\n",
"d442c028feffff15dee39f0378f8100094e21200802e0040feffff15de8f1000c1e3dd29feffff15dee39d0363f810006100c0291e20a20363f810002000004c3f00150000000000ffbbff53"},
// Big class with the rounding-split store and the floor-split adjust.
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
"d442c0283e000014de23be0378f8120000470044deffff15dee3810378f8100094e2120080320040deffff15de8f1000c1e3ff29beffff15dee3bf0363f810006100c0295e000014de23800363f810002000004c3f00150000000000ffa7ff53"},
// Zero low 12 bits drop the ORI from the store, the adjust and the
// epilogue materialisation.
{"bigzero", "TEXT \u00b7bigzero(SB), $4088-0\n\tRET\n",
"d442c028feffff15de03820378f8100094e21200802a0040feffff15de8f1000c103c029feffff1563f810006100c0293e00001463f810002000004c3f00150000000000ffbfff53"},
// Big class whose first constant has a zero high part: a single ORI.
{"big3976", "TEXT \u00b7big3976(SB), $4096-0\n\tRET\n",
"d442c0281e20be0378f8120000470044feffff15dee3810378f8100094e2120080320040feffff15de8f1000c1e3ff29deffff15dee3bf0363f810006100c0293e000014de23800363f810002000004c3f00150000000000ffabff53"},
// Big class at a multiple of 4096: both guard constants lose their
// ORI word.
{"giantlo0", "TEXT \u00b7giantlo0(SB), $4216-0\n\tRET\n",
"d442c0283e00001478f8120000430044feffff1578f8100094e2120080320040feffff15de8f1000c103fe29deffff15de03be0363f810006100c0293e000014de03820363f810002000004c3f00150000000000ffafff53"},
// Non-leaf big frame: the body call plus the LR restore epilogue.
{"callbig", "TEXT \u00b7callbig(SB), $8192-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
"d442c0283e000014de23be0378f81200004f0044deffff15dee3810378f8100094e21200803a0040deffff15de8f1000c1e3ff29beffff15dee3bf0363f810006100c029000000006100c0285e000014de23800363f810002000004c3f00150000000000ff9fff53"},
} { } {
f, errs := parser.Parse("g_loong64.s", tt.src) f, errs := parser.Parse("g_loong64.s", tt.src)
if len(errs) > 0 { if len(errs) > 0 {
@@ -224,3 +254,30 @@ func TestStackGuardBytesLOONG64(t *testing.T) {
} }
} }
} }
// TestStackGuardGOObjInternalCall checks that GOOBJ emission succeeds when a
// guarded function calls a TEXT symbol of the same file, for every arch's
// call relocation kind.
func TestStackGuardGOObjInternalCall(t *testing.T) {
for _, tt := range []struct {
src string
assemble func(*ast.File) (*Image, error)
}{
{"g_amd64.s", AssembleFile},
{"g_arm64.s", AssembleFileARM64},
{"g_riscv64.s", AssembleFileRISCV},
{"g_loong64.s", AssembleFileLOONG64},
} {
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("%s: parse: %v", tt.src, errs)
}
img, err := tt.assemble(f)
if err != nil {
t.Fatalf("%s: assemble: %v", tt.src, err)
}
if _, err := img.GOObject("testpkg", tt.src); err != nil {
t.Errorf("%s: GOObject: %v", tt.src, err)
}
}
}
+22 -20
View File
@@ -18,12 +18,10 @@ import (
// then encoding with resolved branch targets). // then encoding with resolved branch targets).
// //
// The emitted bytes match the Go toolchain's loong64 assembler, which is the // The emitted bytes match the Go toolchain's loong64 assembler, which is the
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch // ground-truth oracle: prologue/epilogue (including the large-frame R30
// encodings and the MOV immediate expansions all follow cmd/internal/obj/ // materialisations), FP/SP frame mapping, the stack-split guard classes, and
// loong64's asmout cases. One deliberate difference: the stack-growth guard // branch encodings all follow cmd/internal/obj/loong64. The morestack block
// (the morestack check in the prologue and the call back into the runtime in // at the end of split functions carries the runtime.morestack_noctxt call.
// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions
// or zero-frame leaves, where the toolchain emits no guard either.
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)
@@ -39,11 +37,12 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
var relocs []Reloc var relocs []Reloc
var spadj []SpadjStep var spadj []SpadjStep
// The prologue (3 instructions when a frame is present) raises the SP // The prologue raises the SP delta by autosize; the boundary is reported
// delta by autosize; the boundary is reported at the third instruction's // after the SP adjust instruction, exactly as the toolchain's pctospadj
// pc, exactly as the toolchain's pctospadj does. // does. The prologue (3 instructions when a frame is present) may
// materialise its store or adjust through R30, which widens it.
if fi.autosize != 0 { if fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: guardLen + 8, Value: fi.autosize}) spadj = append(spadj, SpadjStep{PC: guardLen + (loong64StoreWords(fi.autosize)+loong64AdjustWords(-int64(fi.autosize)))*4, Value: fi.autosize})
} }
// Pass 1: label offsets from the instruction sizes. // Pass 1: label offsets from the instruction sizes.
@@ -99,14 +98,9 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
preCount = len(relocs) preCount = len(relocs)
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line}) lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
// 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
// after the addi.d (one instruction for a leaf, two for a non-leaf // after the frame-deallocating ADDV.
// with the LR restore).
if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 { if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 {
epi := 4 spadj = append(spadj, SpadjStep{PC: pc + loong64EpilogueWords(fi)*4, Value: 0})
if !fi.leaf {
epi = 8
}
spadj = append(spadj, SpadjStep{PC: pc + epi, Value: 0})
} }
out = append(out, code...) out = append(out, code...)
pc += len(code) pc += len(code)
@@ -255,9 +249,9 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
} }
return l64wordLE(uint32(immFromOperand(ops[0]))), nil return l64wordLE(uint32(immFromOperand(ops[0]))), nil
case "JMP", "B": case "JMP", "B":
return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve) return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve, relocs)
case "JAL", "CALL", "BL": case "JAL", "CALL", "BL":
return encodeLOONG64Branch(instr, mnem, pc, offsets, true, resolve) return encodeLOONG64Branch(instr, mnem, pc, offsets, true, resolve, relocs)
case "MOV", "MOVB", "MOVH", "MOVW", "MOVV", "MOVBU", "MOVHU", "MOVWU", "MOVF", "MOVD": case "MOV", "MOVB", "MOVH", "MOVW", "MOVV", "MOVBU", "MOVHU", "MOVWU", "MOVF", "MOVD":
return encodeLOONG64Mov(instr, mnem, fi, relocs) return encodeLOONG64Mov(instr, mnem, fi, relocs)
} }
@@ -516,7 +510,7 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
// //
// JMP/B label → b label JMP/B (rj) → jirl r0, rj, 0 // JMP/B label → b label JMP/B (rj) → jirl r0, rj, 0
// JAL/CALL/BL label → bl label JAL/CALL/BL (rj) → jirl r1, rj, 0 // JAL/CALL/BL label → bl label JAL/CALL/BL (rj) → jirl r1, rj, 0
func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string) ([]byte, error) { func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string, relocs *[]Reloc) ([]byte, error) {
if len(instr.Operands) != 1 { if len(instr.Operands) != 1 {
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(instr.Operands)) return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(instr.Operands))
} }
@@ -533,6 +527,14 @@ func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[stri
} }
return l64wordLE(l64irr16(l64branchTable["JIRL"], 0, rj, rd)), nil return l64wordLE(l64irr16(l64branchTable["JIRL"], 0, rj, rd)), nil
} }
// Direct symbol: sym+off(SB) → bl with an R_CALLLOONG64 relocation (the
// linker fills the offset), as the toolchain does for CALL/BL/JAL.
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" && link {
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelLoong64Branch, Addend: op.Addr.Sym.Offset})
}
return l64wordLE(l64bbl(l64jumpTable["BL"], 0)), nil
}
// Direct: label → b/bl. // Direct: label → b/bl.
target := resolve(l64Label(op)) target := resolve(l64Label(op))
targetOff, ok := offsets[target] targetOff, ok := offsets[target]
+141 -36
View File
@@ -20,14 +20,20 @@ import (
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`). // (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
// A leaf function (no calls) with a zero frame gets no prologue at all. // A leaf function (no calls) with a zero frame gets no prologue at all.
// //
// Prologue (autosize > 0), byte-identical to the toolchain: // Prologue (autosize > 0, small), byte-identical to the toolchain:
// //
// MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe) // MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe)
// ADDV $-autosize, R3 // open the frame // ADDV $-autosize, R3 // open the frame
// MOVV R1, 0(R3) // save LR again at SP (signal-safety) // MOVV R1, 0(R3) // save LR again at SP (signal-safety)
// //
// Large frames (autosize past the 12-bit offset or immediate ranges) expand
// the store and the adjust through REGTMP (R30) exactly as the toolchain's
// assembler does: the store via the rounding LU12IW split, the adjust via
// the floor LU12IW/ORI split.
//
// Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR // Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR
// restore); the RET's jirl r0, r1, 0 follows. // restore; the adjust materialised when the immediate does not fit); the
// RET's jirl r0, r1, 0 follows.
// loong64FrameInfo holds the frame layout derived from a TEXT directive. // loong64FrameInfo holds the frame layout derived from a TEXT directive.
type loong64FrameInfo struct { type loong64FrameInfo struct {
@@ -84,62 +90,108 @@ func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
// loong64GuardLen returns the byte length of the stack-split guard prefix // loong64GuardLen returns the byte length of the stack-split guard prefix
// (zero when the function needs no guard). The big class materialises two // (zero when the function needs no guard). The big class materialises two
// constants through R30. // constants through R30; each materialisation shrinks by one word when the
// constant's low 12 bits are zero.
func loong64GuardLen(fi loong64FrameInfo) int { func loong64GuardLen(fi loong64FrameInfo) int {
if !fi.needSplit { if !fi.needSplit {
return 0 return 0
} }
off := int64(fi.autosize - stackSmall)
switch fi.splitClass { switch fi.splitClass {
case 0: case 0:
return 12 return 12
case 1: case 1:
return 16 if off <= 2048 {
return 16 // ADDV $-off fits the signed 12-bit immediate
}
return 24 // MOVV + LU12IW + ORI + ADDV + SGTU + BEQ
default: default:
return 40 // MOVV + [LU12IW+ORI] + SGTU + BNE + [LU12IW+ORI] + ADDV + SGTU + BEQ // MOVV + [mat] + SGTU + BNE + [mat] + ADDV + SGTU + BEQ
return (6 + loong64MatLen(off) + loong64MatLen(-off)) * 4
} }
} }
// loong64Lu12iOri materialises the 32-bit constant v in rd with the // loong64MatLen reports the word count of materialising v in R30: a value
// toolchain's LU12IW/ORI pair (the ORI reads and writes rd itself). // with a zero high part needs only the ORI (the toolchain's MOVW $v, R30),
func loong64Lu12iOri(rd int, v int64) []uint32 { // one with a zero low part only the LU12IW.
hi := int32(v >> 12) func loong64MatLen(v int64) int {
lo := int32(v & 0xFFF) if v>>12 == 0 || v&0xFFF == 0 {
return []uint32{ return 1
0x0a<<25 | uint32(hi&0xFFFFF)<<5 | uint32(rd),
0x0e<<22 | uint32(lo)<<10 | uint32(rd)<<5 | uint32(rd),
} }
return 2
} }
// loong64MatWords appends the words that materialise v in R30, splitting it
// as v>>12 plus the zero-extended low 12 bits.
func loong64MatWords(ws []uint32, v int64) []uint32 {
hi := v >> 12
lo := v & 0xFFF
if hi == 0 {
return append(ws, l64irr(l64OriOp, int(v), 0, 30))
}
ws = append(ws, l64ir(l64Lu12iwOp, int(hi), 30))
if lo != 0 {
ws = append(ws, l64irr(l64OriOp, int(lo), 30, 30))
}
return ws
}
// The LU12IW and ORI opcode bases (2RI20 and 2RI12 formats); the ORI reads
// and writes rd itself.
const (
l64Lu12iwOp = 0x0a << 25
l64OriOp = 0x0e << 22
)
// loong64Imm12 reports whether v fits a signed 12-bit immediate.
func loong64Imm12(v int64) bool { return v >= -2048 && v <= 2047 }
// loong64GuardBytes emits the stack-split guard prefix. blockStart is the // loong64GuardBytes emits the stack-split guard prefix. blockStart is the
// function-relative address of the morestack call at the end of the function; // function-relative address of the morestack call at the end of the function;
// branch displacements are in instructions. // branch displacements are in instructions and are computed from each
// branch's own position.
func loong64GuardBytes(fi loong64FrameInfo, blockStart int) []byte { func loong64GuardBytes(fi loong64FrameInfo, blockStart int) []byte {
// MOVV 16(g), R20 (g.stackguard0), g = R22. // MOVV 16(g), R20 (g.stackguard0), g = R22.
ws := []uint32{l64irr(l64loadStoreTable["MOVV"].ld, 16, 22, 20)} ws := []uint32{l64irr(l64loadStoreTable["MOVV"].ld, 16, 22, 20)}
off := int64(fi.autosize - stackSmall)
// beq appends BEQ R20, blockStart from the branch's own position.
beq := func() {
ws = append(ws, loong64Beqz(20, int32((blockStart-len(ws)*4)>>2)))
}
switch fi.splitClass { switch fi.splitClass {
case 0: case 0:
// SGTU SP, R20, R20; BEQ R20, more // SGTU SP, R20, R20; BEQ R20, more
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 3, 20, 20)) ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 3, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-8)>>2))) beq()
case 1: case 1:
off := int32(fi.autosize - stackSmall) ws = append(ws, loong64MediumWords(off)...)
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, int(-off), 3, 24))
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20)) ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-12)>>2))) beq()
default: default:
off := int64(fi.autosize - stackSmall) // SGTU $off, SP, R24 catches the SP underflow a huge frame would
movLen := 8 // LU12IW + ORI // cause; BNE jumps to morestack in that case.
ws = append(ws, loong64Lu12iOri(30, off)...) ws = append(ws, loong64MatWords(nil, off)...)
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 30, 3, 24)) ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 30, 3, 24))
ws = append(ws, loong64Bnez(24, int32((blockStart-(8+movLen))>>2))) ws = append(ws, loong64Bnez(24, int32((blockStart-len(ws)*4)>>2)))
ws = append(ws, loong64Lu12iOri(30, -off)...) ws = append(ws, loong64MatWords(nil, -off)...)
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24)) ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24))
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20)) ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
ws = append(ws, loong64Beqz(20, int32((blockStart-loong64GuardLen(fi)+12)>>2))) beq()
} }
return l64WordsLE(ws...) return l64WordsLE(ws...)
} }
// loong64MediumWords emits the medium-class stack check for offset off: the
// ADDV immediate when it fits, otherwise the same sequence with the constant
// materialised in R30.
func loong64MediumWords(off int64) []uint32 {
if off <= 2048 {
return []uint32{l64irr(l64DualTable["ADDV"].imm, int(-off), 3, 24)}
}
ws := loong64MatWords(nil, -off)
return append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24))
}
// loong64Beqz/loong64Bnez build the 21-bit conditional branches against R0 // loong64Beqz/loong64Bnez build the 21-bit conditional branches against R0
// that the toolchain emits for its guard compares. // that the toolchain emits for its guard compares.
func loong64Beqz(rj int, dispInstr int32) uint32 { func loong64Beqz(rj int, dispInstr int32) uint32 {
@@ -150,12 +202,13 @@ func loong64Bnez(rj int, dispInstr int32) uint32 {
return l64ir21(l64branch21Table["BNEZ"], int(dispInstr), rj) return l64ir21(l64branch21Table["BNEZ"], int(dispInstr), rj)
} }
// loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR), // loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR, the
// BL runtime.morestack_noctxt, B back to the function entry. // toolchain's OR R1, R0, R31 expansion), BL runtime.morestack_noctxt, B back
// to the function entry.
func loong64MoreStackBlock(blockStart int) ([]byte, Reloc) { func loong64MoreStackBlock(blockStart int) ([]byte, Reloc) {
ws := []uint32{ ws := []uint32{
l64rrr(l64DualTable["ADD"].rrr, 0, 1, 31), // MOVV R1, R31 (ADD R1, R0, R31) l64rrr(l64DualTable["OR"].rrr, 0, 1, 31), // MOVV R1, R31 (OR R1, R0, R31)
l64bbl(l64jumpTable["BL"], 0), // BL, patched by the linker l64bbl(l64jumpTable["BL"], 0), // BL, patched by the linker
} }
disp := (-(blockStart + 8)) >> 2 disp := (-(blockStart + 8)) >> 2
ws = append(ws, l64bbl(l64jumpTable["B"], int(disp))) ws = append(ws, l64bbl(l64jumpTable["B"], int(disp)))
@@ -185,17 +238,37 @@ func loong64IsLeaf(t *ast.Text) bool {
return true return true
} }
// loong64Prologue returns the prologue bytes for a loong64 function. // loong64Prologue returns the prologue bytes for a loong64 function. When
// the LR store offset leaves the toolchain's 12-bit store range ([-2046,
// 2045], BIG_12 = 2046) or the SP adjust immediate its 12-bit immediate
// range, each switches to the R30 materialisation the assembler expands it
// to: the store uses the rounding %hi/%lo split (LU12IW of (v+2048)>>12,
// REGTMP += SP, store at the raw offset), the adjust the floor split
// (LU12IW, ORI when the low part is non-zero, REGTMP += SP).
func loong64Prologue(fi loong64FrameInfo) []byte { func loong64Prologue(fi loong64FrameInfo) []byte {
if fi.autosize == 0 { if fi.autosize == 0 {
return nil return nil
} }
addiD := l64DualTable["ADDV"].imm addiD := l64DualTable["ADDV"].imm
return l64WordsLE( var ws []uint32
l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3) storeBase := 3
l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3 if fi.autosize > 2046 {
l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3) // The store goes through REGTMP: LU12IW of the rounding split,
) // REGTMP += SP, then the store at REGTMP with the truncated offset.
v := -int64(fi.autosize)
ws = append(ws, l64ir(l64Lu12iwOp, int((v+2048)>>12), 30))
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 3, 30, 30))
storeBase = 30
}
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, storeBase, 1)) // MOVV R1, -autosize(base)
if loong64Imm12(-int64(fi.autosize)) {
ws = append(ws, l64irr(addiD, -fi.autosize, 3, 3)) // ADDV $-autosize, R3
} else {
ws = append(ws, loong64MatWords(nil, -int64(fi.autosize))...)
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 3))
}
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1)) // MOVV R1, 0(R3)
return l64WordsLE(ws...)
} }
// loong64Return returns the bytes for a RET: the epilogue (restore LR and // loong64Return returns the bytes for a RET: the epilogue (restore LR and
@@ -207,14 +280,46 @@ func loong64Return(fi loong64FrameInfo) []byte {
// MOVV 0(R3), R1, restore the link register. // MOVV 0(R3), R1, restore the link register.
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1)) ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1))
} }
// ADDV $autosize, R3, close the frame. // ADDV $autosize, R3, close the frame (materialised when the
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3)) // immediate does not fit).
if loong64Imm12(int64(fi.autosize)) {
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3))
} else {
ws = append(ws, loong64MatWords(nil, int64(fi.autosize))...)
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 3))
}
} }
// jirl r0, r1, 0, return. // jirl r0, r1, 0, return.
ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0)) ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0))
return l64WordsLE(ws...) return l64WordsLE(ws...)
} }
// loong64StoreWords reports the prologue word count of the LR store, and
// loong64AdjustWords the word count of an SP adjust of v: the immediate
// forms when they fit, otherwise the R30 materialisation sequences.
func loong64StoreWords(autosize int) int {
if autosize > 2046 {
return 3
}
return 1
}
func loong64AdjustWords(v int64) int {
if loong64Imm12(v) {
return 1
}
return loong64MatLen(v) + 1
}
// loong64EpilogueWords reports the epilogue word count the RET expands to.
func loong64EpilogueWords(fi loong64FrameInfo) int {
n := loong64AdjustWords(int64(fi.autosize))
if !fi.leaf {
n++
}
return n
}
// loong64ResolvePseudo translates a pseudo-register memory reference into a // loong64ResolvePseudo translates a pseudo-register memory reference into a
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP); // hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
// x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable // x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable