diff --git a/asm/assemble.go b/asm/assemble.go index 1f7dd71..6dd08fb 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -51,6 +51,7 @@ type sbPatch struct { after int name string addend int64 + kind RelocKind } // spadjStep is one stack-adjustment boundary within a function: Value is the @@ -75,13 +76,18 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [ return name } - // Layout: iterate jump sizes to a fixed point. + // Layout: iterate jump sizes to a fixed point. The stack-split guard + // prefix and the trailing morestack block participate in the iteration: + // their conditional branches relax from rel8 to rel32 when the body + // outgrows the short form. long := make([]bool, len(t.Body)) sizes := make([]int, len(t.Body)) offsets := map[string]int{} pcs := make([]int, len(t.Body)) + var guardJBlong, guardJBElong, moreJMPlong bool for { - pos := len(fi.prologue) + guard := fi.guardLen(guardJBlong, guardJBElong) + pos := guard + len(fi.prologue) for i, stmt := range t.Body { switch s := stmt.(type) { case *ast.Label: @@ -96,6 +102,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [ pos += sz } } + bodyLen := pos - (guard + len(fi.prologue)) // Expand any short jump whose displacement no longer fits rel8. changed := false for i, stmt := range t.Body { @@ -121,25 +128,75 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [ changed = true } } + // The guard's conditional branches target the morestack block, which + // starts right after the body: the JBE measures from the end of the + // guard, so its displacement is the prologue plus the body. + if !guardJBElong && !fits8(int64(len(fi.prologue)+bodyLen)) { + guardJBElong = true + changed = true + } + if fi.splitClass == 2 && !guardJBlong { + // The underflow JB sits before the CMPQ; its displacement spans + // the rest of the guard plus the prologue and the body. + jbLen := 2 + if guardJBlong { + jbLen = 6 + } + rest := fi.guardLen(guardJBlong, guardJBElong) - (9 + 3 + 7 + jbLen) + if !fits8(int64(rest + len(fi.prologue) + bodyLen)) { + guardJBlong = true + changed = true + } + } + // The morestack JMP returns to the function start, so its + // displacement is the negated distance from its own end. + if !moreJMPlong { + jmpLen := 2 + if moreJMPlong { + jmpLen = 5 + } + if !fits8(-int64(guard + len(fi.prologue) + bodyLen + 5 + jmpLen)) { + moreJMPlong = true + changed = true + } + } if !changed { break } } - // Pass 2: emit. - out := append([]byte(nil), fi.prologue...) + // Pass 2: emit. The guard comes first, then the prologue, the body and + // the morestack block. + guardLen := fi.guardLen(guardJBlong, guardJBElong) + bodyLen := 0 + { + pos := guardLen + len(fi.prologue) + for i, stmt := range t.Body { + if _, ok := stmt.(*ast.Instr); ok { + pos += sizes[i] + } + } + bodyLen = pos - (guardLen + len(fi.prologue)) + } + var out []byte var patches []sbPatch + if fi.needSplit { + guard, tlsPatch := buildGuard(fi, int32(len(fi.prologue)+bodyLen), int32(fi.guardLen(guardJBlong, guardJBElong)-(9+3+7+2)+len(fi.prologue)+bodyLen)) + out = append(out, guard...) + patches = append(patches, tlsPatch) + } + out = append(out, fi.prologue...) var steps []spadjStep var lines []LineEntry if fi.useFP { // PUSHQ BP saves the return-address-relative base (+8); the MOVQ // changes nothing; SUBQ $size, SP completes the frame. steps = append(steps, - spadjStep{1, 8}, - spadjStep{len(fi.prologue), 8 + fi.size}, + spadjStep{guardLen + 1, 8}, + spadjStep{guardLen + len(fi.prologue), 8 + fi.size}, ) } - pos := len(fi.prologue) + pos := guardLen + len(fi.prologue) for i, stmt := range t.Body { s, ok := stmt.(*ast.Instr) if !ok { @@ -161,11 +218,32 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [ if len(code) != sizes[i] { return nil, nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i]) } + if strings.ToUpper(s.Mnemonic.Text) == "CALL" { + for k := range ps { + ps[k].kind = RelCall + } + } patches = append(patches, ps...) lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line}) out = append(out, code...) pos += len(code) } + if fi.needSplit { + // The morestack block: CALL runtime.morestack_noctxt, then a JMP + // back to the function entry. + jmpLen := 2 + if moreJMPlong { + jmpLen = 5 + } + jmpDisp := -int64(pos + 5 + jmpLen) + suffix, callPatch := buildMoreStack(int32(jmpDisp)) + callPatch.off += pos + callPatch.after = pos + 5 + patches = append(patches, callPatch) + out = append(out, suffix...) + pos += len(suffix) + } + _ = pos return out, patches, offsets, steps, lines, nil } @@ -229,10 +307,31 @@ type frameInfo struct { spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP) prologue []byte epilogue []byte + + // Stack-split guard state (matching the toolchain's stacksplit): needSplit + // is false for NOSPLIT functions and for leaf functions whose frame is + // below StackSmall, which the toolchain auto-marks NOSPLIT. + needSplit bool + splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig + framesize int // the size the guard checks: frame+8 for framed functions } +// Stack-frame size classes from runtime/stack.go. +const ( + stackSmall = 128 + stackBig = 4096 +) + +// sbPatch gains a kind so the emitters can tell CALL and TLS patches from +// plain PC-relative displacements. + // computeFrame derives the frame layout, matching the Go assembler's default -// (a frame pointer is used whenever the function has a non-zero frame). +// (a frame pointer is used whenever the function has a non-zero frame). It +// also decides whether the function needs the stack-split guard, mirroring +// obj6: a NOSPLIT function never splits, and a leaf function whose frame is +// below StackSmall is auto-marked NOSPLIT. One deliberate deviation: the +// toolchain treats zero-argument runtime calls (duffcopy and friends) as +// leaf-compatible; here any CALL makes the function a non-leaf. func computeFrame(t *ast.Text) frameInfo { fi := frameInfo{} if t.Frame != nil && t.Frame.Imm.HasVal { @@ -247,9 +346,141 @@ func computeFrame(t *ast.Text) frameInfo { } else { fi.fpAdjust = 8 // return address only } + + noSplit := false + for _, f := range t.Flags { + if strings.EqualFold(f, "NOSPLIT") { + noSplit = true + } + } + // The toolchain's autoffset: the frame plus the saved base pointer. + framesize := fi.size + if framesize > 0 { + framesize += 8 + } + switch { + case noSplit: + case framesize < stackSmall && !hasCall(t): + // Auto-NOSPLIT, as the toolchain's leaf search concludes. + default: + fi.needSplit = true + fi.framesize = framesize + switch { + case framesize <= stackSmall: + fi.splitClass = 0 + case framesize <= stackBig: + fi.splitClass = 1 + default: + fi.splitClass = 2 + } + } return fi } +// hasCall reports whether the function body contains a CALL instruction. +func hasCall(t *ast.Text) bool { + for _, stmt := range t.Body { + in, ok := stmt.(*ast.Instr) + if !ok { + continue + } + if strings.ToUpper(in.Mnemonic.Text) == "CALL" { + return true + } + } + return false +} + +// guardLen returns the byte length of the stack-split guard prefix. The +// final conditional branch (JBE, and JB in the big class) is 2 bytes in the +// short form and 6 in the long form. +func (fi frameInfo) guardLen(jbLong, jbeLong bool) int { + if !fi.needSplit { + return 0 + } + jb, jbe := 2, 2 + if jbLong { + jb = 6 + } + if jbeLong { + jbe = 6 + } + switch fi.splitClass { + case 0: + return 9 + 4 + jbe + case 1: + return 9 + 8 + 4 + jbe + default: + return 9 + 3 + 7 + jb + 4 + jbe + } +} + +// moreLen returns the byte length of the trailing morestack block: the CALL +// (always rel32) plus the JMP back to the function start. +func moreLen(jmpLong bool) int { + jmp := 2 + if jmpLong { + jmp = 5 + } + return 5 + jmp +} + +// buildGuard emits the stack-split guard prefix. jbeDisp and jbDisp are the +// already-computed displacements of the conditional branches that jump to the +// morestack block (unused in classes without them). The TLS load carries a +// R_TLS_LE patch site at offset 5. +func buildGuard(fi frameInfo, jbeDisp, jbDisp int32) ([]byte, sbPatch) { + out := []byte{ + 0x64, 0x4c, 0x8b, 0x34, 0x25, // MOVQ FS:0, R14 + 0, 0, 0, 0, // TLS slot offset, filled by the linker + } + tls := sbPatch{off: 5, after: 9, kind: RelTLSLE} + jmp := func(op8, op32 byte, disp int32) []byte { + if disp >= -128 && disp <= 127 { + return []byte{op8, byte(disp)} + } + return append([]byte{0x0F, op32}, le32(int64(disp))...) + } + switch fi.splitClass { + case 0: + // CMPQ SP, 16(R14) + out = append(out, 0x49, 0x3b, 0x66, 0x10) + out = append(out, jmp(0x76, 0x86, jbeDisp)...) + case 1: + // LEAQ -(framesize-StackSmall)(SP), R12; CMPQ R12, 16(R14) + out = append(out, 0x4c, 0x8d, 0xa4, 0x24) + out = append(out, le32(-int64(fi.framesize-stackSmall))...) + out = append(out, 0x4d, 0x3b, 0x66, 0x10) + out = append(out, jmp(0x76, 0x86, jbeDisp)...) + default: + // MOVQ SP, R12; SUBQ $(framesize-StackSmall), R12; JB; CMPQ R12, 16(R14) + out = append(out, 0x49, 0x89, 0xe4) + out = append(out, 0x49, 0x81, 0xec) + out = append(out, le32(int64(fi.framesize-stackSmall))...) + out = append(out, jmp(0x72, 0x82, jbDisp)...) + out = append(out, 0x4d, 0x3b, 0x66, 0x10) + out = append(out, jmp(0x76, 0x86, jbeDisp)...) + } + return out, tls +} + +// buildMoreStack emits the trailing block: CALL runtime.morestack_noctxt +// (patched by the linker) and a JMP back to the function start. +func buildMoreStack(jmpDisp int32) ([]byte, sbPatch) { + out := []byte{0xE8, 0, 0, 0, 0} + call := sbPatch{off: 1, after: 5, name: "runtime\u00b7morestack_noctxt", kind: RelCall} + out = append(out, jmpBytes(jmpDisp)...) + return out, call +} + +// jmpBytes encodes a near JMP in the short or long form. +func jmpBytes(disp int32) []byte { + if disp >= -128 && disp <= 127 { + return []byte{0xEB, byte(disp)} + } + return append([]byte{0xE9}, le32(int64(disp))...) +} + // prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP. func prologueBytes(size int) []byte { out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP @@ -333,6 +564,23 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon var ps []sbPatch var err error if isJumpMnemonic(mnem) { + if mnem == "CALL" && isSBCall(s) { + // CALL sym(SB): a rel32 call against a static or external + // symbol, resolved by the file-level layout or the linker. + code, ps, err = encodeSBCall(s, link) + if err != nil { + return nil, nil, err + } + for i := range ps { + ps[i].kind = RelCall + } + body := pc + len(prefix) + for i := range ps { + ps[i].off += body + ps[i].after = body + len(code) + } + return append(prefix, code...), ps, nil + } code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve) } else { code, ps, err = encodeNormal(s, fi, link) @@ -414,6 +662,33 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long } } +// isSBCall reports whether the CALL operand is a symbol reference. +func isSBCall(s *ast.Instr) bool { + return len(s.Operands) == 1 && s.Operands[0].Kind == ast.OpAddr && + s.Operands[0].Addr.Sym != nil && s.Operands[0].Addr.Sym.Pseudo == "SB" +} + +// encodeSBCall encodes CALL sym(SB) as E8 rel32 with a patch site. +func encodeSBCall(s *ast.Instr, link *linkInfo) ([]byte, []sbPatch, error) { + o, err := operandFromAST(s.Operands[0], 8, frameInfo{}, link) + if err != nil { + return nil, nil, err + } + m, ok := o.(sbMem) + if !ok { + return nil, nil, fmt.Errorf("CALL: unsupported operand") + } + e := &enc{} + if err := e.emit(&instr{opcode: []byte{0xE8}, modrm: -1, sib: -1, disp: le32(0), sb: &sbRef{name: m.name, addend: m.addend}}); err != nil { + return nil, nil, err + } + ps := make([]sbPatch, len(e.patches)) + for i, p := range e.patches { + ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend, kind: RelCall} + } + return e.out, ps, nil +} + // labelName extracts a local-label name from a jump operand. func labelName(op *ast.Operand) (string, bool) { if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" && diff --git a/asm/elf.go b/asm/elf.go index 8776e09..09b5db7 100644 --- a/asm/elf.go +++ b/asm/elf.go @@ -42,7 +42,8 @@ const ( sttSection = 3 stInfoShift = 4 - rX8664PC32 = 2 + rX8664PC32 = 2 + rX8664TPOFF32 = 20 ) // elfSym is one symbol-table entry in construction. @@ -125,11 +126,19 @@ func (img *Image) ELFObject() ([]byte, error) { type elfRela struct { off uint64 sym int + typ uint32 addend int64 } var relas []elfRela for _, fn := range img.Funcs { for _, r := range fn.Relocs { + var typ uint32 = rX8664PC32 + if r.Kind == RelTLSLE { + // R_X86_64_TPOFF32 resolves to the local-exec TLS offset and + // carries no symbol. + relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), sym: 0, typ: rX8664TPOFF32}) + continue + } idx, ok := symIdx[r.Name] if !ok { return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) @@ -137,6 +146,7 @@ func (img *Image) ELFObject() ([]byte, error) { relas = append(relas, elfRela{ off: uint64(fn.Offset + r.Off), sym: idx, + typ: typ, // R_X86_64_PC32 computes S + A − P with P the patch site; the // assembler measures the symbol from the instruction end, // After − Off bytes past the field, so the addend carries diff --git a/asm/goobj.go b/asm/goobj.go index 4811d22..f0be041 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -94,10 +94,12 @@ const ( ) // Relocation types (cmd/internal/objabi). -// R_PCREL and R_ADDR are stable across Go versions. +// R_ADDR, R_CALL, R_PCREL and R_TLS_LE are stable across Go versions. const ( - relocPCRel = 14 // R_PCREL relocAddr = 1 // R_ADDR + relocCall = 7 // R_CALL + relocPCRel = 14 // R_PCREL + relocTLSLE = 15 // R_TLS_LE ) // relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the @@ -140,10 +142,20 @@ func isGo127OrLater() bool { // Special package indices for symbol references. const ( - pkgIdxNone = 0x7fffffff - pkgIdxSelf = 0x7ffffffb + pkgIdxNone = 0x7fffffff + pkgIdxSelf = 0x7ffffffb + pkgIdxBuiltin = 0x7ffffffc ) +// goobjBuiltinMorestackNoctxt is the index of runtime.morestack_noctxt in +// cmd/internal/goobj/builtinlist.go of the toolchain the object targets +// (246 since Go 1.25; the list is append-only). +const goobjBuiltinMorestackNoctxt = 246 + +// goobjBuiltinMorestack is the builtin reference the toolchain emits for the +// stack-guard call. +var goobjBuiltinMorestack = "runtime\u00b7morestack_noctxt" + const goobjMagic = "\x00go120ld" // goSym is one symbol definition under construction. @@ -184,8 +196,18 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) { if err != nil { return nil, err } - // amd64: MinLC 1, R_PCREL for the code relocations. - return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 }) + // amd64: MinLC 1, R_PCREL for displacements, R_CALL for calls and + // R_TLS_LE for the stack-guard TLS load. + return img.emitGOObject(pkgPath, srcPath, pre, 1, func(r Reloc) (uint16, uint8) { + switch r.Kind { + case RelCall: + return relocCall, 4 + case RelTLSLE: + return relocTLSLE, 4 + default: + return relocPCRel, 4 + } + }) } // emitGOObject assembles the GOOBJ payload for any architecture. pre is @@ -247,7 +269,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r // their relocations cover whole AUIPC/pcalau12i pairs, so // zeroing r.Off would erase the opcode/register bits the linker // preserves when it patches only the immediate. - if r.Kind != RelPCRel32 { + if r.Kind != RelPCRel32 && r.Kind != RelCall { continue } if r.Off >= 0 && r.Off+4 <= len(code) { @@ -320,6 +342,13 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r ) } + // Index the non-package TEXT definitions by short name for the internal + // call references. + textNpIdx := map[string]int{} + for i, fn := range img.Funcs { + textNpIdx[fn.Name] = fnNpIdx[i] + } + // Resolve external symbol references (cross-package). Build the // package index table and determine each external symbol's SymIdx // by reading the target package's export data. @@ -327,10 +356,20 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r var extPkgIdx map[string]int var extSymIdx map[string]int if len(img.Externals) > 0 { - var err error - extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals) - if err != nil { - return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err) + // The morestack call is a builtin reference, not a resolved external. + var need []string + for _, n := range img.Externals { + if n == goobjBuiltinMorestack { + continue + } + need = append(need, n) + } + if len(need) > 0 { + var err error + extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need) + if err != nil { + return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err) + } } } @@ -342,6 +381,31 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r si := len(defs) + fnNpIdx[i] for _, r := range fn.Relocs { typ, size := relocField(r) + if r.Kind == RelTLSLE { + // The TLS load has no symbol: {0, 0} is the nil ref. + var rec [23]byte + binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) + rec[4] = size + binary.LittleEndian.PutUint16(rec[5:], typ) + binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) + binary.LittleEndian.PutUint32(rec[15:], 0) + binary.LittleEndian.PutUint32(rec[19:], 0) + symRelocs[si] = append(symRelocs[si], rec[:]...) + continue + } + if r.External && r.Kind == RelCall && r.Name == goobjBuiltinMorestack { + // The stack-guard morestack call uses the toolchain's + // builtin reference. + var rec [23]byte + binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) + rec[4] = size + binary.LittleEndian.PutUint16(rec[5:], typ) + binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) + binary.LittleEndian.PutUint32(rec[15:], pkgIdxBuiltin) + binary.LittleEndian.PutUint32(rec[19:], goobjBuiltinMorestackNoctxt) + symRelocs[si] = append(symRelocs[si], rec[:]...) + continue + } if r.External { // Split package-qualified name: "runtime·morestack" → runtime, morestack. pkg, name := splitQualified(r.Name) @@ -366,16 +430,24 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r symRelocs[si] = append(symRelocs[si], rec[:]...) continue } + pkg := uint32(pkgIdxSelf) di, ok := defIdx[r.Name] if !ok { - return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name) + // 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 { + return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name) + } + pkg = pkgIdxNone + di = ni } var rec [23]byte binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) rec[4] = size // field width binary.LittleEndian.PutUint16(rec[5:], typ) binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) - binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf) + binary.LittleEndian.PutUint32(rec[15:], pkg) binary.LittleEndian.PutUint32(rec[19:], uint32(di)) symRelocs[si] = append(symRelocs[si], rec[:]...) } diff --git a/asm/guard_test.go b/asm/guard_test.go new file mode 100644 index 0000000..33cd08e --- /dev/null +++ b/asm/guard_test.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "bytes" + "encoding/hex" + "testing" + + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64, +// verified with go tool objdump): the stack-split guard classes, the morestack +// block and the auto-NOSPLIT leaf behaviour. +func TestStackGuardBytes(t *testing.T) { + for _, tt := range []struct { + name string + src string + want string + }{ + {"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n", + "554889e54883ec104883c4105dc3"}, + {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", + "644c8b3425000000004c8da42478ffffff4d3b66107614554889e54881ec000100004881c4000100005dc3e800000000ebce"}, + {"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n", + "644c8b3425000000004989e44981ec881f0000721a4d3b66107614554889e54881ec002000004881c4002000005dc3e800000000ebca"}, + {"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n", + "644c8b342500000000493b66107613554889e54883ec10e8000000004883c4105dc3e800000000ebd7"}, + {"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n", + "554889e54883ec104883c4105dc3"}, + } { + f, errs := parser.Parse("g_amd64.s", tt.src) + if len(errs) > 0 { + t.Fatalf("%s: parse: %v", tt.name, errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("%s: assemble: %v", tt.name, err) + } + fn := img.Funcs[0] + // The toolchain's object leaves every relocation field zero for the + // linker, while the gasm image resolves file-internal references, so + // the comparison masks the patch sites the way verify's ground truth + // does. + 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) + } + } +} + +// TestStackGuardRelocs checks the guard's patch sites: the TLS slot and the +// morestack call. +func TestStackGuardRelocs(t *testing.T) { + f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tRET\n") + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("assemble: %v", err) + } + relocs := img.Funcs[0].Relocs + if len(relocs) != 2 { + t.Fatalf("relocs = %d, want 2", len(relocs)) + } + tls, call := relocs[0], relocs[1] + if tls.Kind != RelTLSLE || tls.Off != 5 || tls.Name != "" || tls.External { + t.Errorf("tls reloc = %+v, want RelTLSLE at 5 with no symbol", tls) + } + if call.Kind != RelCall || call.Name != "runtime\u00b7morestack_noctxt" || !call.External { + t.Errorf("call reloc = %+v, want RelCall to runtime.morestack_noctxt", call) + } +} + +// TestStackGuardGOObj emissions succeed with the guard's TLS and builtin +// references in play. +func TestStackGuardGOObj(t *testing.T) { + f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tCALL \u00b7helper(SB)\n\tRET\nTEXT \u00b7helper(SB), NOSPLIT, $0\n\tRET\n") + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("assemble: %v", err) + } + obj, err := img.GOObject("testpkg", "g_amd64.s") + if err != nil { + t.Fatalf("GOObject: %v", err) + } + if !bytes.Contains(obj, []byte("go120ld")) { + t.Fatal("object lacks the GOOBJ magic") + } +} diff --git a/asm/link.go b/asm/link.go index 9e3d047..fd8be91 100644 --- a/asm/link.go +++ b/asm/link.go @@ -91,6 +91,8 @@ type RelocKind int const ( RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64) + RelCall // R_CALL: CALL to a function symbol (amd64) + RelTLSLE // R_TLS_LE: local-exec TLS load, no symbol (amd64 guard) RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair) RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair) RelRISCVJal // R_RISCV_JAL (J-type call) @@ -148,6 +150,7 @@ func AssembleFile(f *ast.File) (*Image, error) { link := &linkInfo{symbols: known, allowExternal: true} img := &Image{Symbols: map[string]int{}} + textOff := map[string]int{} type asmFunc struct { name string patches []sbPatch @@ -185,6 +188,7 @@ func AssembleFile(f *ast.File) (*Image, error) { for _, s := range steps { fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value}) } + textOff[t.Name.Name] = len(img.Code) img.Funcs = append(img.Funcs, fl) img.Code = append(img.Code, code...) funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches}) @@ -217,13 +221,27 @@ func AssembleFile(f *ast.File) (*Image, error) { base := img.Funcs[i].Offset code := img.Code[base : base+img.Funcs[i].Size] for _, p := range fn.patches { - reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend} + reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend, Kind: p.kind} + if p.kind == RelTLSLE { + // The TLS slot has no symbol: the linker fills the offset + // from the runtime's TLS layout. + img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc) + continue + } if imgOff, ok := img.Symbols[p.name]; ok { rel := int64(imgOff) + p.addend - int64(base+p.after) if rel < -1<<31 || rel >= 1<<31 { return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name) } copy(code[p.off:p.off+4], le32(rel)) + } else if imgOff, ok := textOff[p.name]; ok { + // A CALL to a TEXT function of the same file: resolve the + // displacement against the function's layout position. + rel := int64(imgOff) + p.addend - int64(base+p.after) + if rel < -1<<31 || rel >= 1<<31 { + return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name) + } + copy(code[p.off:p.off+4], le32(rel)) } else { reloc.External = true externals[p.name] = true