feat(asm): emit the amd64 stack-split guard and morestack block

This commit is contained in:
2026-09-14 20:35:55 +02:00
parent 89fa6ea15e
commit 1d8e68c574
5 changed files with 500 additions and 23 deletions
+283 -8
View File
@@ -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 == "" &&
+10
View File
@@ -43,6 +43,7 @@ const (
stInfoShift = 4
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
+79 -7
View File
@@ -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
@@ -142,8 +144,18 @@ func isGo127OrLater() bool {
const (
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,12 +356,22 @@ 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 {
// 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(img.Externals)
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
if err != nil {
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
}
}
}
// Relocations, per defined symbol in definition order (package defs,
// then non-package defs).
@@ -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 {
// 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[:]...)
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
}
}
+19 -1
View File
@@ -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