| // 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 = ^loader.Sym(0) | |
| // 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.StackNosplit(*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) | |
| } | |
| } | |
| } | |