github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/link/ld/stackcheck.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package ld
     6  
     7  import (
     8  	"fmt"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/go-asm/go/buildcfg"
    13  	"github.com/go-asm/go/cmd/link/loader"
    14  	"github.com/go-asm/go/cmd/obj"
    15  	"github.com/go-asm/go/cmd/objabi"
    16  )
    17  
    18  type stackCheck struct {
    19  	ctxt      *Link
    20  	ldr       *loader.Loader
    21  	morestack loader.Sym
    22  	callSize  int // The number of bytes added by a CALL
    23  
    24  	// height records the maximum number of bytes a function and
    25  	// its callees can add to the stack without a split check.
    26  	height map[loader.Sym]int16
    27  
    28  	// graph records the out-edges from each symbol. This is only
    29  	// populated on a second pass if the first pass reveals an
    30  	// over-limit function.
    31  	graph map[loader.Sym][]stackCheckEdge
    32  }
    33  
    34  type stackCheckEdge struct {
    35  	growth int        // Stack growth in bytes at call to target
    36  	target loader.Sym // 0 for stack growth without a call
    37  }
    38  
    39  // stackCheckCycle is a sentinel stored in the height map to detect if
    40  // we've found a cycle. This is effectively an "infinite" stack
    41  // height, so we use the closest value to infinity that we can.
    42  const stackCheckCycle int16 = 1<<15 - 1
    43  
    44  // stackCheckIndirect is a sentinel Sym value used to represent the
    45  // target of an indirect/closure call.
    46  const stackCheckIndirect loader.Sym = ^loader.Sym(0)
    47  
    48  // doStackCheck walks the call tree to check that there is always
    49  // enough stack space for call frames, especially for a chain of
    50  // nosplit functions.
    51  //
    52  // It walks all functions to accumulate the number of bytes they can
    53  // grow the stack by without a split check and checks this against the
    54  // limit.
    55  func (ctxt *Link) doStackCheck() {
    56  	sc := newStackCheck(ctxt, false)
    57  
    58  	// limit is number of bytes a splittable function ensures are
    59  	// available on the stack. If any call chain exceeds this
    60  	// depth, the stack check test fails.
    61  	//
    62  	// The call to morestack in every splittable function ensures
    63  	// that there are at least StackLimit bytes available below SP
    64  	// when morestack returns.
    65  	limit := objabi.StackNosplit(*flagRace) - sc.callSize
    66  	if buildcfg.GOARCH == "arm64" {
    67  		// Need an extra 8 bytes below SP to save FP.
    68  		limit -= 8
    69  	}
    70  
    71  	// Compute stack heights without any back-tracking information.
    72  	// This will almost certainly succeed and we can simply
    73  	// return. If it fails, we do a second pass with back-tracking
    74  	// to produce a good error message.
    75  	//
    76  	// This accumulates stack heights bottom-up so it only has to
    77  	// visit every function once.
    78  	var failed []loader.Sym
    79  	for _, s := range ctxt.Textp {
    80  		if sc.check(s) > limit {
    81  			failed = append(failed, s)
    82  		}
    83  	}
    84  
    85  	if len(failed) > 0 {
    86  		// Something was over-limit, so now we do the more
    87  		// expensive work to report a good error. First, for
    88  		// the over-limit functions, redo the stack check but
    89  		// record the graph this time.
    90  		sc = newStackCheck(ctxt, true)
    91  		for _, s := range failed {
    92  			sc.check(s)
    93  		}
    94  
    95  		// Find the roots of the graph (functions that are not
    96  		// called by any other function).
    97  		roots := sc.findRoots()
    98  
    99  		// Find and report all paths that go over the limit.
   100  		// This accumulates stack depths top-down. This is
   101  		// much less efficient because we may have to visit
   102  		// the same function multiple times at different
   103  		// depths, but lets us find all paths.
   104  		for _, root := range roots {
   105  			ctxt.Errorf(root, "nosplit stack over %d byte limit", limit)
   106  			chain := []stackCheckChain{{stackCheckEdge{0, root}, false}}
   107  			sc.report(root, limit, &chain)
   108  		}
   109  	}
   110  }
   111  
   112  func newStackCheck(ctxt *Link, graph bool) *stackCheck {
   113  	sc := &stackCheck{
   114  		ctxt:      ctxt,
   115  		ldr:       ctxt.loader,
   116  		morestack: ctxt.loader.Lookup("runtime.morestack", 0),
   117  		height:    make(map[loader.Sym]int16, len(ctxt.Textp)),
   118  	}
   119  	// Compute stack effect of a CALL operation. 0 on LR machines.
   120  	// 1 register pushed on non-LR machines.
   121  	if !ctxt.Arch.HasLR {
   122  		sc.callSize = ctxt.Arch.RegSize
   123  	}
   124  
   125  	if graph {
   126  		// We're going to record the call graph.
   127  		sc.graph = make(map[loader.Sym][]stackCheckEdge)
   128  	}
   129  
   130  	return sc
   131  }
   132  
   133  func (sc *stackCheck) symName(sym loader.Sym) string {
   134  	switch sym {
   135  	case stackCheckIndirect:
   136  		return "indirect"
   137  	case 0:
   138  		return "leaf"
   139  	}
   140  	return fmt.Sprintf("%s<%d>", sc.ldr.SymName(sym), sc.ldr.SymVersion(sym))
   141  }
   142  
   143  // check returns the stack height of sym. It populates sc.height and
   144  // sc.graph for sym and every function in its call tree.
   145  func (sc *stackCheck) check(sym loader.Sym) int {
   146  	if h, ok := sc.height[sym]; ok {
   147  		// We've already visited this symbol or we're in a cycle.
   148  		return int(h)
   149  	}
   150  	// Store the sentinel so we can detect cycles.
   151  	sc.height[sym] = stackCheckCycle
   152  	// Compute and record the height and optionally edges.
   153  	h, edges := sc.computeHeight(sym, *flagDebugNosplit || sc.graph != nil)
   154  	if h > int(stackCheckCycle) { // Prevent integer overflow
   155  		h = int(stackCheckCycle)
   156  	}
   157  	sc.height[sym] = int16(h)
   158  	if sc.graph != nil {
   159  		sc.graph[sym] = edges
   160  	}
   161  
   162  	if *flagDebugNosplit {
   163  		for _, edge := range edges {
   164  			fmt.Printf("nosplit: %s +%d", sc.symName(sym), edge.growth)
   165  			if edge.target == 0 {
   166  				// Local stack growth or leaf function.
   167  				fmt.Printf("\n")
   168  			} else {
   169  				fmt.Printf(" -> %s\n", sc.symName(edge.target))
   170  			}
   171  		}
   172  	}
   173  
   174  	return h
   175  }
   176  
   177  // computeHeight returns the stack height of sym. If graph is true, it
   178  // also returns the out-edges of sym.
   179  //
   180  // Caching is applied to this in check. Call check instead of calling
   181  // this directly.
   182  func (sc *stackCheck) computeHeight(sym loader.Sym, graph bool) (int, []stackCheckEdge) {
   183  	ldr := sc.ldr
   184  
   185  	// Check special cases.
   186  	if sym == sc.morestack {
   187  		// morestack looks like it calls functions, but they
   188  		// either happen only when already on the system stack
   189  		// (where there is ~infinite space), or after
   190  		// switching to the system stack. Hence, its stack
   191  		// height on the user stack is 0.
   192  		return 0, nil
   193  	}
   194  	if sym == stackCheckIndirect {
   195  		// Assume that indirect/closure calls are always to
   196  		// splittable functions, so they just need enough room
   197  		// to call morestack.
   198  		return sc.callSize, []stackCheckEdge{{sc.callSize, sc.morestack}}
   199  	}
   200  
   201  	// Ignore calls to external functions. Assume that these calls
   202  	// are only ever happening on the system stack, where there's
   203  	// plenty of room.
   204  	if ldr.AttrExternal(sym) {
   205  		return 0, nil
   206  	}
   207  	if info := ldr.FuncInfo(sym); !info.Valid() { // also external
   208  		return 0, nil
   209  	}
   210  
   211  	// Track the maximum height of this function and, if we're
   212  	// recording the graph, its out-edges.
   213  	var edges []stackCheckEdge
   214  	maxHeight := 0
   215  	ctxt := sc.ctxt
   216  	// addEdge adds a stack growth out of this function to
   217  	// function "target" or, if target == 0, a local stack growth
   218  	// within the function.
   219  	addEdge := func(growth int, target loader.Sym) {
   220  		if graph {
   221  			edges = append(edges, stackCheckEdge{growth, target})
   222  		}
   223  		height := growth
   224  		if target != 0 { // Don't walk into the leaf "edge"
   225  			height += sc.check(target)
   226  		}
   227  		if height > maxHeight {
   228  			maxHeight = height
   229  		}
   230  	}
   231  
   232  	if !ldr.IsNoSplit(sym) {
   233  		// Splittable functions start with a call to
   234  		// morestack, after which their height is 0. Account
   235  		// for the height of the call to morestack.
   236  		addEdge(sc.callSize, sc.morestack)
   237  		return maxHeight, edges
   238  	}
   239  
   240  	// This function is nosplit, so it adjusts SP without a split
   241  	// check.
   242  	//
   243  	// Walk through SP adjustments in function, consuming relocs
   244  	// and following calls.
   245  	maxLocalHeight := 0
   246  	relocs, ri := ldr.Relocs(sym), 0
   247  	pcsp := obj.NewPCIter(uint32(ctxt.Arch.MinLC))
   248  	for pcsp.Init(ldr.Data(ldr.Pcsp(sym))); !pcsp.Done; pcsp.Next() {
   249  		// pcsp.value is in effect for [pcsp.pc, pcsp.nextpc).
   250  		height := int(pcsp.Value)
   251  		if height > maxLocalHeight {
   252  			maxLocalHeight = height
   253  		}
   254  
   255  		// Process calls in this span.
   256  		for ; ri < relocs.Count(); ri++ {
   257  			r := relocs.At(ri)
   258  			if uint32(r.Off()) >= pcsp.NextPC {
   259  				break
   260  			}
   261  			t := r.Type()
   262  			if t.IsDirectCall() || t == objabi.R_CALLIND {
   263  				growth := height + sc.callSize
   264  				var target loader.Sym
   265  				if t == objabi.R_CALLIND {
   266  					target = stackCheckIndirect
   267  				} else {
   268  					target = r.Sym()
   269  				}
   270  				addEdge(growth, target)
   271  			}
   272  		}
   273  	}
   274  	if maxLocalHeight > maxHeight {
   275  		// This is either a leaf function, or the function
   276  		// grew its stack to larger than the maximum call
   277  		// height between calls. Either way, record that local
   278  		// stack growth.
   279  		addEdge(maxLocalHeight, 0)
   280  	}
   281  
   282  	return maxHeight, edges
   283  }
   284  
   285  func (sc *stackCheck) findRoots() []loader.Sym {
   286  	// Collect all nodes.
   287  	nodes := make(map[loader.Sym]struct{})
   288  	for k := range sc.graph {
   289  		nodes[k] = struct{}{}
   290  	}
   291  
   292  	// Start a DFS from each node and delete all reachable
   293  	// children. If we encounter an unrooted cycle, this will
   294  	// delete everything in that cycle, so we detect this case and
   295  	// track the lowest-numbered node encountered in the cycle and
   296  	// put that node back as a root.
   297  	var walk func(origin, sym loader.Sym) (cycle bool, lowest loader.Sym)
   298  	walk = func(origin, sym loader.Sym) (cycle bool, lowest loader.Sym) {
   299  		if _, ok := nodes[sym]; !ok {
   300  			// We already deleted this node.
   301  			return false, 0
   302  		}
   303  		delete(nodes, sym)
   304  
   305  		if origin == sym {
   306  			// We found an unrooted cycle. We already
   307  			// deleted all children of this node. Walk
   308  			// back up, tracking the lowest numbered
   309  			// symbol in this cycle.
   310  			return true, sym
   311  		}
   312  
   313  		// Delete children of this node.
   314  		for _, out := range sc.graph[sym] {
   315  			if c, l := walk(origin, out.target); c {
   316  				cycle = true
   317  				if lowest == 0 {
   318  					// On first cycle detection,
   319  					// add sym to the set of
   320  					// lowest-numbered candidates.
   321  					lowest = sym
   322  				}
   323  				if l < lowest {
   324  					lowest = l
   325  				}
   326  			}
   327  		}
   328  		return
   329  	}
   330  	for k := range nodes {
   331  		// Delete all children of k.
   332  		for _, out := range sc.graph[k] {
   333  			if cycle, lowest := walk(k, out.target); cycle {
   334  				// This is an unrooted cycle so we
   335  				// just deleted everything. Put back
   336  				// the lowest-numbered symbol.
   337  				nodes[lowest] = struct{}{}
   338  			}
   339  		}
   340  	}
   341  
   342  	// Sort roots by height. This makes the result deterministic
   343  	// and also improves the error reporting.
   344  	var roots []loader.Sym
   345  	for k := range nodes {
   346  		roots = append(roots, k)
   347  	}
   348  	sort.Slice(roots, func(i, j int) bool {
   349  		h1, h2 := sc.height[roots[i]], sc.height[roots[j]]
   350  		if h1 != h2 {
   351  			return h1 > h2
   352  		}
   353  		// Secondary sort by Sym.
   354  		return roots[i] < roots[j]
   355  	})
   356  	return roots
   357  }
   358  
   359  type stackCheckChain struct {
   360  	stackCheckEdge
   361  	printed bool
   362  }
   363  
   364  func (sc *stackCheck) report(sym loader.Sym, depth int, chain *[]stackCheckChain) {
   365  	// Walk the out-edges of sym. We temporarily pull the edges
   366  	// out of the graph to detect cycles and prevent infinite
   367  	// recursion.
   368  	edges, ok := sc.graph[sym]
   369  	isCycle := !(ok || sym == 0)
   370  	delete(sc.graph, sym)
   371  	for _, out := range edges {
   372  		*chain = append(*chain, stackCheckChain{out, false})
   373  		sc.report(out.target, depth-out.growth, chain)
   374  		*chain = (*chain)[:len(*chain)-1]
   375  	}
   376  	sc.graph[sym] = edges
   377  
   378  	// If we've reached the end of a chain and it went over the
   379  	// stack limit or was a cycle that would eventually go over,
   380  	// print the whole chain.
   381  	//
   382  	// We should either be in morestack (which has no out-edges)
   383  	// or the sentinel 0 Sym "called" from a leaf function (which
   384  	// has no out-edges), or we came back around a cycle (possibly
   385  	// to ourselves) and edges was temporarily nil'd.
   386  	if len(edges) == 0 && (depth < 0 || isCycle) {
   387  		var indent string
   388  		for i := range *chain {
   389  			ent := &(*chain)[i]
   390  			if ent.printed {
   391  				// Already printed on an earlier part
   392  				// of this call tree.
   393  				continue
   394  			}
   395  			ent.printed = true
   396  
   397  			if i == 0 {
   398  				// chain[0] is just the root function,
   399  				// not a stack growth.
   400  				fmt.Printf("%s\n", sc.symName(ent.target))
   401  				continue
   402  			}
   403  
   404  			indent = strings.Repeat("    ", i)
   405  			fmt.Print(indent)
   406  			// Grows the stack X bytes and (maybe) calls Y.
   407  			fmt.Printf("grows %d bytes", ent.growth)
   408  			if ent.target == 0 {
   409  				// Not a call, just a leaf. Print nothing.
   410  			} else {
   411  				fmt.Printf(", calls %s", sc.symName(ent.target))
   412  			}
   413  			fmt.Printf("\n")
   414  		}
   415  		// Print how far over this chain went.
   416  		if isCycle {
   417  			fmt.Printf("%sinfinite cycle\n", indent)
   418  		} else {
   419  			fmt.Printf("%s%d bytes over limit\n", indent, -depth)
   420  		}
   421  	}
   422  }