diff --git a/asm/arm64_frame.go b/asm/arm64_frame.go index 6252751..1ca20ef 100644 --- a/asm/arm64_frame.go +++ b/asm/arm64_frame.go @@ -182,7 +182,7 @@ func arm64Prologue(fi arm64FrameInfo) []byte { // arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value // 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 { if imm <= 0xFFF { return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)} @@ -191,7 +191,7 @@ func arm64SubImmWords(imm uint32, rd uint32) []uint32 { if err != 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. @@ -203,7 +203,7 @@ func arm64AddImmWords(imm uint32, rd uint32) []uint32 { if err != 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 @@ -326,6 +326,13 @@ func arm64DPSRWords(base uint32, rm, rn, rd uint32) uint32 { 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. func wordsOf(b []byte) []uint32 { ws := make([]uint32, 0, len(b)/4) @@ -359,7 +366,7 @@ func arm64GuardBytes(fi arm64FrameInfo, blockStart int) []byte { } ws = append(ws, wordsOf(mov)...) 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, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17 ws = append(ws, br(8+ml+8, a64CondLS)) diff --git a/asm/elfloong64.go b/asm/elfloong64.go index a1dd0df..e6ec9f1 100644 --- a/asm/elfloong64.go +++ b/asm/elfloong64.go @@ -16,6 +16,7 @@ const ( // LoongArch relocation types (the ELF psABI). rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i) 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 @@ -95,8 +96,11 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) { return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) } typ := uint32(rLarchPCALAHI20) - if r.Kind == RelLoong64AddrLo { + switch r.Kind { + case RelLoong64AddrLo: typ = rLarchPCALALO12 + case RelLoong64Branch: + typ = rLarchB26 } relas = append(relas, elfRela{ off: uint64(fn.Offset + r.Off), diff --git a/asm/elfloong64_test.go b/asm/elfloong64_test.go index a53d9aa..227f8a1 100644 --- a/asm/elfloong64_test.go +++ b/asm/elfloong64_test.go @@ -198,3 +198,45 @@ TEXT ·nop(SB), NOSPLIT, $0 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)) + } + } +} diff --git a/asm/goobj.go b/asm/goobj.go index b470652..ac73064 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -156,6 +156,16 @@ const goobjBuiltinMorestackNoctxt = 246 // stack-guard call. 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" // 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 // non-package definition table. 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) } pkg = pkgIdxNone diff --git a/asm/guard_test.go b/asm/guard_test.go index ed4c6cb..3109d9e 100644 --- a/asm/guard_test.go +++ b/asm/guard_test.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "testing" + "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) @@ -116,7 +117,7 @@ func TestStackGuardBytesARM64(t *testing.T) { {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", "900b40f9f14302d13f0210eb09010054f44304d19dfa3fa99f020091fd2300d1fd230491ff430491c0035fd6e3031eaa00000000f3ffff17"}, {"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", "900b40f9ff6330eb09010054fe0f1ef8fd831ff8fd2300d100000000fd835ff8fe0742f8c0035fd6e3031eaa00000000f4ffff17"}, {"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, -// 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. +// loong64): every guard class (including the medium class with the +// materialised constant and the big class with the ORI-less constants), the +// auto-NOSPLIT leaf behaviour, the large-frame R30 prologue/epilogue forms +// and the morestack block. Relocation fields are masked. func TestStackGuardBytesLOONG64(t *testing.T) { for _, tt := range []struct { name string @@ -199,9 +201,37 @@ func TestStackGuardBytesLOONG64(t *testing.T) { {"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n", "61a0ff2963a0ff026100c0296360c0022000004c"}, {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", - "d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00100000000000ffd7ff53"}, + "d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00150000000000ffd7ff53"}, {"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n", "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) 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) + } + } +} diff --git a/asm/loong64_assemble.go b/asm/loong64_assemble.go index 80deb9c..4e57c88 100644 --- a/asm/loong64_assemble.go +++ b/asm/loong64_assemble.go @@ -18,12 +18,10 @@ import ( // then encoding with resolved branch targets). // // The emitted bytes match the Go toolchain's loong64 assembler, which is the -// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch -// encodings and the MOV immediate expansions all follow cmd/internal/obj/ -// loong64's asmout cases. One deliberate difference: the stack-growth guard -// (the morestack check in the prologue and the call back into the runtime in -// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions -// or zero-frame leaves, where the toolchain emits no guard either. +// ground-truth oracle: prologue/epilogue (including the large-frame R30 +// materialisations), FP/SP frame mapping, the stack-split guard classes, and +// branch encodings all follow cmd/internal/obj/loong64. The morestack block +// at the end of split functions carries the runtime.morestack_noctxt call. func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) { fi := loong64ComputeFrame(t) prologue := loong64Prologue(fi) @@ -39,11 +37,12 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, var relocs []Reloc var spadj []SpadjStep - // The prologue (3 instructions when a frame is present) raises the SP - // delta by autosize; the boundary is reported at the third instruction's - // pc, exactly as the toolchain's pctospadj does. + // The prologue raises the SP delta by autosize; the boundary is reported + // after the SP adjust instruction, exactly as the toolchain's pctospadj + // 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 { - 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. @@ -99,14 +98,9 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, preCount = len(relocs) lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line}) // 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 - // with the LR restore). + // after the frame-deallocating ADDV. if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 { - epi := 4 - if !fi.leaf { - epi = 8 - } - spadj = append(spadj, SpadjStep{PC: pc + epi, Value: 0}) + spadj = append(spadj, SpadjStep{PC: pc + loong64EpilogueWords(fi)*4, Value: 0}) } out = append(out, 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 case "JMP", "B": - return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve) + return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve, relocs) 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": 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 // 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 { 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 } + // 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. target := resolve(l64Label(op)) targetOff, ok := offsets[target] diff --git a/asm/loong64_frame.go b/asm/loong64_frame.go index 255db3a..9cbe295 100644 --- a/asm/loong64_frame.go +++ b/asm/loong64_frame.go @@ -20,14 +20,20 @@ import ( // (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. // -// 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) // ADDV $-autosize, R3 // open the frame // 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 -// 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. type loong64FrameInfo struct { @@ -84,62 +90,108 @@ func loong64ComputeFrame(t *ast.Text) loong64FrameInfo { // 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. +// constants through R30; each materialisation shrinks by one word when the +// constant's low 12 bits are zero. func loong64GuardLen(fi loong64FrameInfo) int { if !fi.needSplit { return 0 } + off := int64(fi.autosize - stackSmall) switch fi.splitClass { case 0: return 12 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: - 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 -// 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), +// loong64MatLen reports the word count of materialising v in R30: a value +// with a zero high part needs only the ORI (the toolchain's MOVW $v, R30), +// one with a zero low part only the LU12IW. +func loong64MatLen(v int64) int { + if v>>12 == 0 || v&0xFFF == 0 { + return 1 } + 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 // 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 { // MOVV 16(g), R20 (g.stackguard0), g = R22. 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 { 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))) + beq() case 1: - off := int32(fi.autosize - stackSmall) - ws = append(ws, l64irr(l64DualTable["ADDV"].imm, int(-off), 3, 24)) + ws = append(ws, loong64MediumWords(off)...) ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20)) - ws = append(ws, loong64Beqz(20, int32((blockStart-12)>>2))) + beq() default: - off := int64(fi.autosize - stackSmall) - movLen := 8 // LU12IW + ORI - ws = append(ws, loong64Lu12iOri(30, off)...) + // SGTU $off, SP, R24 catches the SP underflow a huge frame would + // cause; BNE jumps to morestack in that case. + ws = append(ws, loong64MatWords(nil, 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, loong64Bnez(24, int32((blockStart-len(ws)*4)>>2))) + ws = append(ws, loong64MatWords(nil, -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))) + beq() } 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 // that the toolchain emits for its guard compares. 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) } -// loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR), -// BL runtime.morestack_noctxt, B back to the function entry. +// loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR, the +// toolchain's OR R1, R0, R31 expansion), 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 + l64rrr(l64DualTable["OR"].rrr, 0, 1, 31), // MOVV R1, R31 (OR R1, R0, R31) + l64bbl(l64jumpTable["BL"], 0), // BL, patched by the linker } disp := (-(blockStart + 8)) >> 2 ws = append(ws, l64bbl(l64jumpTable["B"], int(disp))) @@ -185,17 +238,37 @@ func loong64IsLeaf(t *ast.Text) bool { 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 { if fi.autosize == 0 { return nil } addiD := l64DualTable["ADDV"].imm - return l64WordsLE( - l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3) - l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3 - l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3) - ) + var ws []uint32 + storeBase := 3 + if fi.autosize > 2046 { + // 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 @@ -207,14 +280,46 @@ func loong64Return(fi loong64FrameInfo) []byte { // MOVV 0(R3), R1, restore the link register. ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1)) } - // ADDV $autosize, R3, close the frame. - ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3)) + // ADDV $autosize, R3, close the frame (materialised when the + // 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. ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0)) 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 // hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP); // x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable