diff options
Diffstat (limited to 'src/cmd/link/internal/ld/stackcheck.go')
-rw-r--r-- | src/cmd/link/internal/ld/stackcheck.go | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/src/cmd/link/internal/ld/stackcheck.go b/src/cmd/link/internal/ld/stackcheck.go new file mode 100644 index 0000000..c82dafe --- /dev/null +++ b/src/cmd/link/internal/ld/stackcheck.go @@ -0,0 +1,421 @@ +// 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(*flagRace) - 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) + } + } +} |