feat(asm): emit the amd64 stack-split guard and morestack block
This commit is contained in:
+283
-8
@@ -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 == "" &&
|
||||
|
||||
Reference in New Issue
Block a user