blob: f0e13670681c77055653a4e7ecddcda739b04419 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ld
import (
"cmd/internal/obj"
"cmd/internal/objabi"
"cmd/link/internal/loader"
"fmt"
"internal/buildcfg"
"sort"
"strings"
)
type stackCheck struct {
ctxt *Link
ldr *loader.Loader
morestack loader.Sym
callSize int // The number of bytes added by a CALL
// height records the maximum number of bytes a function and
// its callees can add to the stack without a split check.
height map[loader.Sym]int16
// graph records the out-edges from each symbol. This is only
// populated on a second pass if the first pass reveals an
// over-limit function.
graph map[loader.Sym][]stackCheckEdge
}
type stackCheckEdge struct {
growth int // Stack growth in bytes at call to target
target loader.Sym // 0 for stack growth without a call
}
// stackCheckCycle is a sentinel stored in the height map to detect if
// we've found a cycle. This is effectively an "infinite" stack
// height, so we use the closest value to infinity that we can.
const stackCheckCycle int16 = 1<<15 - 1
// stackCheckIndirect is a sentinel Sym value used to represent the
// target of an indirect/closure call.
const stackCheckIndirect loader.Sym = -1
// doStackCheck walks the call tree to check that there is always
// enough stack space for call frames, especially for a chain of
// nosplit functions.
//
// It walks all functions to accumulate the number of bytes they can
// grow the stack by without a split check and checks this against the
// limit.
func (ctxt *Link) doStackCheck() {
sc := newStackCheck(ctxt, false)
// limit is number of bytes a splittable function ensures are
// available on the stack. If any call chain exceeds this
// depth, the stack check test fails.
//
// The call to morestack in every splittable function ensures
// that there are at least StackLimit bytes available below SP
// when morestack returns.
limit := objabi.StackLimit - sc.callSize
if buildcfg.GOARCH == "arm64" {
// Need an extra 8 bytes below SP to save FP.
limit -= 8
}
// Compute stack heights without any back-tracking information.
// This will almost certainly succeed and we can simply
// return. If it fails, we do a second pass with back-tracking
// to produce a good error message.
//
// This accumulates stack heights bottom-up so it only has to
// visit every function once.
var failed []loader.Sym
for _, s := range ctxt.Textp {
if sc.check(s) > limit {
failed = append(failed, s)
}
}
if len(failed) > 0 {
// Something was over-limit, so now we do the more
// expensive work to report a good error. First, for
// the over-limit functions, redo the stack check but
// record the graph this time.
sc = newStackCheck(ctxt, true)
for _, s := range failed {
sc.check(s)
}
// Find the roots of the graph (functions that are not
// called by any other function).
roots := sc.findRoots()
// Find and report all paths that go over the limit.
// This accumulates stack depths top-down. This is
// much less efficient because we may have to visit
// the same function multiple times at different
// depths, but lets us find all paths.
for _, root := range roots {
ctxt.Errorf(root, "nosplit stack over %d byte limit", limit)
chain := []stackCheckChain{{stackCheckEdge{0, root}, false}}
sc.report(root, limit, &chain)
}
}
}
func newStackCheck(ctxt *Link, graph bool) *stackCheck {
sc := &stackCheck{
ctxt: ctxt,
ldr: ctxt.loader,
morestack: ctxt.loader.Lookup("runtime.morestack", 0),
height: make(map[loader.Sym]int16, len(ctxt.Textp)),
}
// Compute stack effect of a CALL operation. 0 on LR machines.
// 1 register pushed on non-LR machines.
if !ctxt.Arch.HasLR {
sc.callSize = ctxt.Arch.RegSize
}
if graph {
// We're going to record the call graph.
sc.graph = make(map[loader.Sym][]stackCheckEdge)
}
return sc
}
func (sc *stackCheck) symName(sym loader.Sym) string {
switch sym {
case stackCheckIndirect:
return "indirect"
case 0:
return "leaf"
}
return fmt.Sprintf("%s<%d>", sc.ldr.SymName(sym), sc.ldr.SymVersion(sym))
}
// check returns the stack height of sym. It populates sc.height and
// sc.graph for sym and every function in its call tree.
func (sc *stackCheck) check(sym loader.Sym) int {
if h, ok := sc.height[sym]; ok {
// We've already visited this symbol or we're in a cycle.
return int(h)
}
// Store the sentinel so we can detect cycles.
sc.height[sym] = stackCheckCycle
// Compute and record the height and optionally edges.
h, edges := sc.computeHeight(sym, *flagDebugNosplit || sc.graph != nil)
if h > int(stackCheckCycle) { // Prevent integer overflow
h = int(stackCheckCycle)
}
sc.height[sym] = int16(h)
if sc.graph != nil {
sc.graph[sym] = edges
}
if *flagDebugNosplit {
for _, edge := range edges {
fmt.Printf("nosplit: %s +%d", sc.symName(sym), edge.growth)
if edge.target == 0 {
// Local stack growth or leaf function.
fmt.Printf("\n")
} else {
fmt.Printf(" -> %s\n", sc.symName(edge.target))
}
}
}
return h
}
// computeHeight returns the stack height of sym. If graph is true, it
// also returns the out-edges of sym.
//
// Caching is applied to this in check. Call check instead of calling
// this directly.
func (sc *stackCheck) computeHeight(sym loader.Sym, graph bool) (int, []stackCheckEdge) {
ldr := sc.ldr
// Check special cases.
if sym == sc.morestack {
// morestack looks like it calls functions, but they
// either happen only when already on the system stack
// (where there is ~infinite space), or after
// switching to the system stack. Hence, its stack
// height on the user stack is 0.
return 0, nil
}
if sym == stackCheckIndirect {
// Assume that indirect/closure calls are always to
// splittable functions, so they just need enough room
// to call morestack.
return sc.callSize, []stackCheckEdge{{sc.callSize, sc.morestack}}
}
// Ignore calls to external functions. Assume that these calls
// are only ever happening on the system stack, where there's
// plenty of room.
if ldr.AttrExternal(sym) {
return 0, nil
}
if info := ldr.FuncInfo(sym); !info.Valid() { // also external
return 0, nil
}
// Track the maximum height of this function and, if we're
// recording the graph, its out-edges.
var edges []stackCheckEdge
maxHeight := 0
ctxt := sc.ctxt
// addEdge adds a stack growth out of this function to
// function "target" or, if target == 0, a local stack growth
// within the function.
addEdge := func(growth int, target loader.Sym) {
if graph {
edges = append(edges, stackCheckEdge{growth, target})
}
height := growth
if target != 0 { // Don't walk into the leaf "edge"
height += sc.check(target)
}
if height > maxHeight {
maxHeight = height
}
}
if !ldr.IsNoSplit(sym) {
// Splittable functions start with a call to
// morestack, after which their height is 0. Account
// for the height of the call to morestack.
addEdge(sc.callSize, sc.morestack)
return maxHeight, edges
}
// This function is nosplit, so it adjusts SP without a split
// check.
//
// Walk through SP adjustments in function, consuming relocs
// and following calls.
maxLocalHeight := 0
relocs, ri := ldr.Relocs(sym), 0
pcsp := obj.NewPCIter(uint32(ctxt.Arch.MinLC))
for pcsp.Init(ldr.Data(ldr.Pcsp(sym))); !pcsp.Done; pcsp.Next() {
// pcsp.value is in effect for [pcsp.pc, pcsp.nextpc).
height := int(pcsp.Value)
if height > maxLocalHeight {
maxLocalHeight = height
}
// Process calls in this span.
for ; ri < relocs.Count(); ri++ {
r := relocs.At(ri)
if uint32(r.Off()) >= pcsp.NextPC {
break
}
t := r.Type()
if t.IsDirectCall() || t == objabi.R_CALLIND {
growth := height + sc.callSize
var target loader.Sym
if t == objabi.R_CALLIND {
target = stackCheckIndirect
} else {
target = r.Sym()
}
addEdge(growth, target)
}
}
}
if maxLocalHeight > maxHeight {
// This is either a leaf function, or the function
// grew its stack to larger than the maximum call
// height between calls. Either way, record that local
// stack growth.
addEdge(maxLocalHeight, 0)
}
return maxHeight, edges
}
func (sc *stackCheck) findRoots() []loader.Sym {
// Collect all nodes.
nodes := make(map[loader.Sym]struct{})
for k := range sc.graph {
nodes[k] = struct{}{}
}
// Start a DFS from each node and delete all reachable
// children. If we encounter an unrooted cycle, this will
// delete everything in that cycle, so we detect this case and
// track the lowest-numbered node encountered in the cycle and
// put that node back as a root.
var walk func(origin, sym loader.Sym) (cycle bool, lowest loader.Sym)
walk = func(origin, sym loader.Sym) (cycle bool, lowest loader.Sym) {
if _, ok := nodes[sym]; !ok {
// We already deleted this node.
return false, 0
}
delete(nodes, sym)
if origin == sym {
// We found an unrooted cycle. We already
// deleted all children of this node. Walk
// back up, tracking the lowest numbered
// symbol in this cycle.
return true, sym
}
// Delete children of this node.
for _, out := range sc.graph[sym] {
if c, l := walk(origin, out.target); c {
cycle = true
if lowest == 0 {
// On first cycle detection,
// add sym to the set of
// lowest-numbered candidates.
lowest = sym
}
if l < lowest {
lowest = l
}
}
}
return
}
for k := range nodes {
// Delete all children of k.
for _, out := range sc.graph[k] {
if cycle, lowest := walk(k, out.target); cycle {
// This is an unrooted cycle so we
// just deleted everything. Put back
// the lowest-numbered symbol.
nodes[lowest] = struct{}{}
}
}
}
// Sort roots by height. This makes the result deterministic
// and also improves the error reporting.
var roots []loader.Sym
for k := range nodes {
roots = append(roots, k)
}
sort.Slice(roots, func(i, j int) bool {
h1, h2 := sc.height[roots[i]], sc.height[roots[j]]
if h1 != h2 {
return h1 > h2
}
// Secondary sort by Sym.
return roots[i] < roots[j]
})
return roots
}
type stackCheckChain struct {
stackCheckEdge
printed bool
}
func (sc *stackCheck) report(sym loader.Sym, depth int, chain *[]stackCheckChain) {
// Walk the out-edges of sym. We temporarily pull the edges
// out of the graph to detect cycles and prevent infinite
// recursion.
edges, ok := sc.graph[sym]
isCycle := !(ok || sym == 0)
delete(sc.graph, sym)
for _, out := range edges {
*chain = append(*chain, stackCheckChain{out, false})
sc.report(out.target, depth-out.growth, chain)
*chain = (*chain)[:len(*chain)-1]
}
sc.graph[sym] = edges
// If we've reached the end of a chain and it went over the
// stack limit or was a cycle that would eventually go over,
// print the whole chain.
//
// We should either be in morestack (which has no out-edges)
// or the sentinel 0 Sym "called" from a leaf function (which
// has no out-edges), or we came back around a cycle (possibly
// to ourselves) and edges was temporarily nil'd.
if len(edges) == 0 && (depth < 0 || isCycle) {
var indent string
for i := range *chain {
ent := &(*chain)[i]
if ent.printed {
// Already printed on an earlier part
// of this call tree.
continue
}
ent.printed = true
if i == 0 {
// chain[0] is just the root function,
// not a stack growth.
fmt.Printf("%s\n", sc.symName(ent.target))
continue
}
indent = strings.Repeat(" ", i)
fmt.Print(indent)
// Grows the stack X bytes and (maybe) calls Y.
fmt.Printf("grows %d bytes", ent.growth)
if ent.target == 0 {
// Not a call, just a leaf. Print nothing.
} else {
fmt.Printf(", calls %s", sc.symName(ent.target))
}
fmt.Printf("\n")
}
// Print how far over this chain went.
if isCycle {
fmt.Printf("%sinfinite cycle\n", indent)
} else {
fmt.Printf("%s%d bytes over limit\n", indent, -depth)
}
}
}