]> Cypherpunks.ru repositories - gostls13.git/commitdiff
cmd/link: faster algorithm for nosplit stack checking, better errors
authorAustin Clements <austin@google.com>
Fri, 1 Apr 2022 19:51:12 +0000 (15:51 -0400)
committerAustin Clements <austin@google.com>
Tue, 19 Apr 2022 15:59:26 +0000 (15:59 +0000)
The linker performs a global analysis of all nosplit call chains to
check they fit in the stack space ensured by splittable functions.
That analysis has two problems right now:

1. It's inefficient. It performs a top-down analysis, starting with
every nosplit function and the nosplit stack limit and walking *down*
the call graph to compute how much stack remains at every call. As a
result, it visits the same functions over and over, often with
different remaining stack depths. This approach is historical: this
check was originally written in C and this approach avoided the need
for any interesting data structures.

2. If some call chain is over the limit, it only reports a single call
chain. As a result, if the check does fail, you often wind up playing
whack-a-mole by guessing where the problem is in the one chain, trying
to reduce the stack size, and then seeing if the link works or reports
a different path.

This CL completely rewrites the nosplit stack check. It now uses a
bottom-up analysis, computing the maximum stack height required by
every function's call tree. This visits every function exactly once,
making it much more efficient. It uses slightly more heap space for
intermediate storage, but still very little in the scheme of the
overall link. For example, when linking cmd/go, the new algorithm
virtually eliminates the time spent in this pass, and reduces overall
link time:

           │   before    │                after                │
           │   sec/op    │   sec/op     vs base                │
Dostkcheck   7.926m ± 4%   1.831m ± 6%  -76.90% (p=0.000 n=20)
TotalTime    301.3m ± 1%   296.4m ± 3%   -1.62% (p=0.040 n=20)

           │    before    │                 after                  │
           │     B/op     │     B/op       vs base                 │
Dostkcheck   40.00Ki ± 0%   212.15Ki ± 0%  +430.37% (p=0.000 n=20)

Most of this time is spent analyzing the runtime, so for larger
binaries, the total time saved is roughly the same, and proportionally
less of the overall link.

If the new implementation finds an error, it redoes the analysis,
switching to preferring quality of error reporting over performance.
For error reporting, it computes stack depths top-down (like the old
algorithm), and reports *all* paths that are over the stack limit,
presented as a tree for compactness. For example, this is the output
from a simple test case from test/nosplit with two over-limit paths
from f1:

        main.f1: nosplit stack overflow
        main.f1
            grows 768 bytes, calls main.f2
                grows 56 bytes, calls main.f4
                    grows 48 bytes
                    80 bytes over limit
            grows 768 bytes, calls main.f3
                grows 104 bytes
                80 bytes over limit

While we're here, we do a few nice cleanups:

- We add a debug output flag, which will be useful for understanding
  what our nosplit chains look like and which ones are close to
  running over.

- We move the implementation out of the fog of lib.go to its own file.

- The implementation is generally more Go-like and less C-like.

Change-Id: If1ab31197f5215475559b93695c44a01bd16e276
Reviewed-on: https://go-review.googlesource.com/c/go/+/398176
Run-TryBot: Austin Clements <austin@google.com>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/cmd/link/internal/ld/lib.go
src/cmd/link/internal/ld/main.go
src/cmd/link/internal/ld/stackcheck.go [new file with mode: 0644]
src/cmd/link/internal/ld/stackcheck_test.go [new file with mode: 0644]
src/cmd/link/internal/ld/testdata/stackcheck/main.go [new file with mode: 0644]
src/cmd/link/internal/ld/testdata/stackcheck/main.s [new file with mode: 0644]
test/nosplit.go

index e26b1e56ddd283c3d2254c5bf31acf6c8125168e..d995f7676bc75dc3369f198d46036b56e0524e87 100644 (file)
@@ -34,7 +34,6 @@ import (
        "bytes"
        "cmd/internal/bio"
        "cmd/internal/goobj"
-       "cmd/internal/obj"
        "cmd/internal/objabi"
        "cmd/internal/sys"
        "cmd/link/internal/loadelf"
@@ -2343,223 +2342,6 @@ func addsection(ldr *loader.Loader, arch *sys.Arch, seg *sym.Segment, name strin
        return sect
 }
 
-type chain struct {
-       sym   loader.Sym
-       up    *chain
-       limit int // limit on entry to sym
-}
-
-func callsize(ctxt *Link) int {
-       if ctxt.Arch.HasLR {
-               return 0
-       }
-       return ctxt.Arch.RegSize
-}
-
-type stkChk struct {
-       ldr       *loader.Loader
-       ctxt      *Link
-       morestack loader.Sym
-       done      loader.Bitmap
-}
-
-// Walk the call tree and check that there is always enough stack space
-// for the call frames, especially for a chain of nosplit functions.
-func (ctxt *Link) dostkcheck() {
-       ldr := ctxt.loader
-       sc := stkChk{
-               ldr:       ldr,
-               ctxt:      ctxt,
-               morestack: ldr.Lookup("runtime.morestack", 0),
-               done:      loader.MakeBitmap(ldr.NSym()),
-       }
-
-       // Every splitting function ensures that there are at least StackLimit
-       // bytes available below SP when the splitting prologue finishes.
-       // If the splitting function calls F, then F begins execution with
-       // at least StackLimit - callsize() bytes available.
-       // Check that every function behaves correctly with this amount
-       // of stack, following direct calls in order to piece together chains
-       // of non-splitting functions.
-       var ch chain
-       ch.limit = objabi.StackLimit - callsize(ctxt)
-       if buildcfg.GOARCH == "arm64" {
-               // need extra 8 bytes below SP to save FP
-               ch.limit -= 8
-       }
-
-       // Check every function, but do the nosplit functions in a first pass,
-       // to make the printed failure chains as short as possible.
-       for _, s := range ctxt.Textp {
-               if ldr.IsNoSplit(s) {
-                       ch.sym = s
-                       sc.check(&ch, 0)
-               }
-       }
-
-       for _, s := range ctxt.Textp {
-               if !ldr.IsNoSplit(s) {
-                       ch.sym = s
-                       sc.check(&ch, 0)
-               }
-       }
-}
-
-func (sc *stkChk) check(up *chain, depth int) int {
-       limit := up.limit
-       s := up.sym
-       ldr := sc.ldr
-       ctxt := sc.ctxt
-
-       // Don't duplicate work: only need to consider each
-       // function at top of safe zone once.
-       top := limit == objabi.StackLimit-callsize(ctxt)
-       if top {
-               if sc.done.Has(s) {
-                       return 0
-               }
-               sc.done.Set(s)
-       }
-
-       if depth > 500 {
-               sc.ctxt.Errorf(s, "nosplit stack check too deep")
-               sc.broke(up, 0)
-               return -1
-       }
-
-       if ldr.AttrExternal(s) {
-               // external function.
-               // should never be called directly.
-               // onlyctxt.Diagnose the direct caller.
-               // TODO(mwhudson): actually think about this.
-               // TODO(khr): disabled for now. Calls to external functions can only happen on the g0 stack.
-               // See the trampolines in src/runtime/sys_darwin_$ARCH.go.
-               //if depth == 1 && ldr.SymType(s) != sym.SXREF && !ctxt.DynlinkingGo() &&
-               //      ctxt.BuildMode != BuildModeCArchive && ctxt.BuildMode != BuildModePIE && ctxt.BuildMode != BuildModeCShared && ctxt.BuildMode != BuildModePlugin {
-               //      Errorf(s, "call to external function")
-               //}
-               return -1
-       }
-       info := ldr.FuncInfo(s)
-       if !info.Valid() { // external function. see above.
-               return -1
-       }
-
-       if limit < 0 {
-               sc.broke(up, limit)
-               return -1
-       }
-
-       // morestack looks like it calls functions,
-       // but it switches the stack pointer first.
-       if s == sc.morestack {
-               return 0
-       }
-
-       var ch chain
-       ch.up = up
-
-       if !ldr.IsNoSplit(s) {
-               // Ensure we have enough stack to call morestack.
-               ch.limit = limit - callsize(ctxt)
-               ch.sym = sc.morestack
-               if sc.check(&ch, depth+1) < 0 {
-                       return -1
-               }
-               if !top {
-                       return 0
-               }
-               // Raise limit to allow frame.
-               locals := info.Locals()
-               limit = objabi.StackLimit + int(locals) + int(ctxt.Arch.FixedFrameSize)
-       }
-
-       // Walk through sp adjustments in function, consuming relocs.
-       relocs := ldr.Relocs(s)
-       var ch1 chain
-       pcsp := obj.NewPCIter(uint32(ctxt.Arch.MinLC))
-       ri := 0
-       for pcsp.Init(ldr.Data(ldr.Pcsp(s))); !pcsp.Done; pcsp.Next() {
-               // pcsp.value is in effect for [pcsp.pc, pcsp.nextpc).
-
-               // Check stack size in effect for this span.
-               if int32(limit)-pcsp.Value < 0 {
-                       sc.broke(up, int(int32(limit)-pcsp.Value))
-                       return -1
-               }
-
-               // Process calls in this span.
-               for ; ri < relocs.Count(); ri++ {
-                       r := relocs.At(ri)
-                       if uint32(r.Off()) >= pcsp.NextPC {
-                               break
-                       }
-                       t := r.Type()
-                       switch {
-                       case t.IsDirectCall():
-                               ch.limit = int(int32(limit) - pcsp.Value - int32(callsize(ctxt)))
-                               ch.sym = r.Sym()
-                               if sc.check(&ch, depth+1) < 0 {
-                                       return -1
-                               }
-
-                       // Indirect call. Assume it is a call to a splitting function,
-                       // so we have to make sure it can call morestack.
-                       // Arrange the data structures to report both calls, so that
-                       // if there is an error, stkprint shows all the steps involved.
-                       case t == objabi.R_CALLIND:
-                               ch.limit = int(int32(limit) - pcsp.Value - int32(callsize(ctxt)))
-                               ch.sym = 0
-                               ch1.limit = ch.limit - callsize(ctxt) // for morestack in called prologue
-                               ch1.up = &ch
-                               ch1.sym = sc.morestack
-                               if sc.check(&ch1, depth+2) < 0 {
-                                       return -1
-                               }
-                       }
-               }
-       }
-
-       return 0
-}
-
-func (sc *stkChk) broke(ch *chain, limit int) {
-       sc.ctxt.Errorf(ch.sym, "nosplit stack overflow")
-       sc.print(ch, limit)
-}
-
-func (sc *stkChk) print(ch *chain, limit int) {
-       ldr := sc.ldr
-       ctxt := sc.ctxt
-       var name string
-       if ch.sym != 0 {
-               name = fmt.Sprintf("%s<%d>", ldr.SymName(ch.sym), ldr.SymVersion(ch.sym))
-               if ldr.IsNoSplit(ch.sym) {
-                       name += " (nosplit)"
-               }
-       } else {
-               name = "function pointer"
-       }
-
-       if ch.up == nil {
-               // top of chain. ch.sym != 0.
-               if ldr.IsNoSplit(ch.sym) {
-                       fmt.Printf("\t%d\tassumed on entry to %s\n", ch.limit, name)
-               } else {
-                       fmt.Printf("\t%d\tguaranteed after split check in %s\n", ch.limit, name)
-               }
-       } else {
-               sc.print(ch.up, ch.limit+callsize(ctxt))
-               if !ctxt.Arch.HasLR {
-                       fmt.Printf("\t%d\ton entry to %s\n", ch.limit, name)
-               }
-       }
-
-       if ch.limit != limit {
-               fmt.Printf("\t%d\tafter %s uses %d\n", limit, name, ch.limit-limit)
-       }
-}
-
 func usage() {
        fmt.Fprintf(os.Stderr, "usage: link [options] main.o\n")
        objabi.Flagprint(os.Stderr)
index fa95a7acf23d5f40b5e1cde07e976a79e5387b30..c52e6e909d84761505c5078a474dc45bcc1c8c1e 100644 (file)
@@ -93,6 +93,7 @@ var (
        flagInterpreter   = flag.String("I", "", "use `linker` as ELF dynamic linker")
        FlagDebugTramp    = flag.Int("debugtramp", 0, "debug trampolines")
        FlagDebugTextSize = flag.Int("debugtextsize", 0, "debug text section max size")
+       flagDebugNosplit  = flag.Bool("debugnosplit", false, "dump nosplit call graph")
        FlagStrictDups    = flag.Int("strictdups", 0, "sanity check duplicate symbol contents during object file reading (1=warn 2=err).")
        FlagRound         = flag.Int("R", -1, "set address rounding `quantum`")
        FlagTextAddr      = flag.Int64("T", -1, "set text segment `address`")
@@ -283,8 +284,8 @@ func Main(arch *sys.Arch, theArch Arch) {
        bench.Start("callgraph")
        ctxt.callgraph()
 
-       bench.Start("dostkcheck")
-       ctxt.dostkcheck()
+       bench.Start("doStackCheck")
+       ctxt.doStackCheck()
 
        bench.Start("mangleTypeSym")
        ctxt.mangleTypeSym()
diff --git a/src/cmd/link/internal/ld/stackcheck.go b/src/cmd/link/internal/ld/stackcheck.go
new file mode 100644 (file)
index 0000000..520e4d6
--- /dev/null
@@ -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 - 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 overflow")
+                       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)
+               }
+       }
+}
diff --git a/src/cmd/link/internal/ld/stackcheck_test.go b/src/cmd/link/internal/ld/stackcheck_test.go
new file mode 100644 (file)
index 0000000..21dbf2b
--- /dev/null
@@ -0,0 +1,89 @@
+// 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/objabi"
+       "cmd/internal/sys"
+       "fmt"
+       "internal/testenv"
+       "os"
+       "os/exec"
+       "regexp"
+       "testing"
+)
+
+// See also $GOROOT/test/nosplit.go for multi-platform edge case tests.
+
+func TestStackCheckOutput(t *testing.T) {
+       testenv.MustHaveGoBuild(t)
+       t.Parallel()
+
+       cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", os.DevNull, "./testdata/stackcheck")
+       // The rules for computing frame sizes on all of the
+       // architectures are complicated, so just do this on amd64.
+       cmd.Env = append(os.Environ(), "GOARCH=amd64")
+       outB, err := cmd.CombinedOutput()
+
+       if err == nil {
+               t.Fatalf("expected link to fail")
+       }
+       out := string(outB)
+
+       t.Logf("linker output:\n%s", out)
+
+       // Construct expected stanzas
+       arch := sys.ArchAMD64
+       call := 0
+       if !arch.HasLR {
+               call = arch.RegSize
+       }
+       limit := objabi.StackLimit - call
+
+       wantMap := map[string]string{
+               "main.startSelf": fmt.Sprintf(
+                       `main.startSelf<0>
+    grows 1008 bytes
+    %d bytes over limit
+`, 1008-limit),
+               "main.startChain": fmt.Sprintf(
+                       `main.startChain<0>
+    grows 32 bytes, calls main.chain0<0>
+        grows 48 bytes, calls main.chainEnd<0>
+            grows 1008 bytes
+            %d bytes over limit
+    grows 32 bytes, calls main.chain2<0>
+        grows 80 bytes, calls main.chainEnd<0>
+            grows 1008 bytes
+            %d bytes over limit
+`, 32+48+1008-limit, 32+80+1008-limit),
+               "main.startRec": `main.startRec<0>
+    grows 8 bytes, calls main.startRec0<0>
+        grows 8 bytes, calls main.startRec<0>
+        infinite cycle
+`,
+       }
+
+       // Parse stanzas
+       stanza := regexp.MustCompile(`^(.*): nosplit stack overflow\n(.*\n(?: .*\n)*)`)
+       // Strip comments from cmd/go
+       out = regexp.MustCompile(`(?m)^#.*\n`).ReplaceAllString(out, "")
+       for len(out) > 0 {
+               m := stanza.FindStringSubmatch(out)
+               if m == nil {
+                       t.Fatalf("unexpected output:\n%s", out)
+               }
+               out = out[len(m[0]):]
+               fn := m[1]
+               got := m[2]
+
+               want, ok := wantMap[fn]
+               if !ok {
+                       t.Errorf("unexpected function: %s", fn)
+               } else if want != got {
+                       t.Errorf("want:\n%sgot:\n%s", want, got)
+               }
+       }
+}
diff --git a/src/cmd/link/internal/ld/testdata/stackcheck/main.go b/src/cmd/link/internal/ld/testdata/stackcheck/main.go
new file mode 100644 (file)
index 0000000..b708cc5
--- /dev/null
@@ -0,0 +1,20 @@
+// 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 main
+
+func main() { asmMain() }
+
+func asmMain()
+
+func startSelf()
+
+func startChain()
+func chain0()
+func chain1()
+func chain2()
+func chainEnd()
+
+func startRec()
+func startRec0()
diff --git a/src/cmd/link/internal/ld/testdata/stackcheck/main.s b/src/cmd/link/internal/ld/testdata/stackcheck/main.s
new file mode 100644 (file)
index 0000000..10f6a3f
--- /dev/null
@@ -0,0 +1,40 @@
+// 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.
+
+#define NOSPLIT 7
+
+TEXT ·asmMain(SB),0,$0-0
+       CALL ·startSelf(SB)
+       CALL ·startChain(SB)
+       CALL ·startRec(SB)
+       RET
+
+// Test reporting of basic over-the-limit
+TEXT ·startSelf(SB),NOSPLIT,$1000-0
+       RET
+
+// Test reporting of multiple over-the-limit chains
+TEXT ·startChain(SB),NOSPLIT,$16-0
+       CALL ·chain0(SB)
+       CALL ·chain1(SB)
+       CALL ·chain2(SB)
+       RET
+TEXT ·chain0(SB),NOSPLIT,$32-0
+       CALL ·chainEnd(SB)
+       RET
+TEXT ·chain1(SB),NOSPLIT,$48-0 // Doesn't go over
+       RET
+TEXT ·chain2(SB),NOSPLIT,$64-0
+       CALL ·chainEnd(SB)
+       RET
+TEXT ·chainEnd(SB),NOSPLIT,$1000-0 // Should be reported twice
+       RET
+
+// Test reporting of rootless recursion
+TEXT ·startRec(SB),NOSPLIT,$0-0
+       CALL ·startRec0(SB)
+       RET
+TEXT ·startRec0(SB),NOSPLIT,$0-0
+       CALL ·startRec(SB)
+       RET
index 7e0fd4e7916e8984cb1826f0413e8ca9a9805af2..9cedb93ec360436c045af5291c724d19eba7126d 100644 (file)
@@ -51,7 +51,8 @@ var tests = `
 start 0
 
 # Large frame marked nosplit is always wrong.
-start 10000 nosplit
+# Frame is so large it overflows cmd/link's int16.
+start 100000 nosplit
 REJECT
 
 # Calling a large frame is okay.