github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/stacksize/stacksize.go (about)

     1  // Package stacksize tries to determine the call graph for ELF binaries and
     2  // tries to parse stack size information from DWARF call frame information.
     3  package stacksize
     4  
     5  import (
     6  	"debug/elf"
     7  	"encoding/binary"
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"sort"
    12  )
    13  
    14  // set to true to print information useful for debugging
    15  const debugPrint = false
    16  
    17  // SizeType indicates whether a stack or frame size could be determined and if
    18  // not, why.
    19  type SizeType uint8
    20  
    21  // Results after trying to determine the stack size of a function in the call
    22  // graph. The goal is to find a maximum (bounded) stack size, but sometimes this
    23  // is not possible for some reasons such as recursion or indirect calls.
    24  const (
    25  	Undefined SizeType = iota // not yet calculated
    26  	Unknown                   // child has unknown stack size
    27  	Bounded                   // stack size is fixed at compile time (no recursion etc)
    28  	Recursive
    29  	IndirectCall
    30  )
    31  
    32  func (s SizeType) String() string {
    33  	switch s {
    34  	case Undefined:
    35  		return "undefined"
    36  	case Unknown:
    37  		return "unknown"
    38  	case Bounded:
    39  		return "bounded"
    40  	case Recursive:
    41  		return "recursive"
    42  	case IndirectCall:
    43  		return "indirect call"
    44  	default:
    45  		return "<?>"
    46  	}
    47  }
    48  
    49  // CallNode is a node in the call graph (that is, a function). Because this is
    50  // determined after linking, there may be multiple names for a single function
    51  // (due to aliases). It is also possible multiple functions have the same name
    52  // (but are in fact different), for example for static functions in C.
    53  type CallNode struct {
    54  	Names            []string
    55  	Address          uint64      // address at which the function is linked (without T bit on ARM)
    56  	Size             uint64      // symbol size, in bytes
    57  	Children         []*CallNode // functions this function calls
    58  	FrameSize        uint64      // frame size, if FrameSizeType is Bounded
    59  	FrameSizeType    SizeType    // can be Undefined or Bounded
    60  	stackSize        uint64
    61  	stackSizeType    SizeType
    62  	missingFrameInfo *CallNode // the child function that is the cause for not being able to determine the stack size
    63  }
    64  
    65  func (n *CallNode) String() string {
    66  	if n == nil {
    67  		return "<nil>"
    68  	}
    69  	return n.Names[0]
    70  }
    71  
    72  // CallGraph parses the ELF file and reads DWARF call frame information to
    73  // determine frame sizes for each function, as far as that's possible. Because
    74  // at this point it is not possible to determine indirect calls, a list of
    75  // indirect function calling functions needs to be supplied separately.
    76  //
    77  // This function does not attempt to determine the stack size for functions.
    78  // This is done by calling StackSize on a function in the call graph.
    79  func CallGraph(f *elf.File, callsIndirectFunction []string) (map[string][]*CallNode, error) {
    80  	// Sanity check that there is exactly one symbol table.
    81  	// Multiple symbol tables are possible, but aren't yet supported below.
    82  	numSymbolTables := 0
    83  	for _, section := range f.Sections {
    84  		if section.Type == elf.SHT_SYMTAB {
    85  			numSymbolTables++
    86  		}
    87  	}
    88  	if numSymbolTables != 1 {
    89  		return nil, fmt.Errorf("expected exactly one symbol table, got %d", numSymbolTables)
    90  	}
    91  
    92  	// Collect all symbols in the executable.
    93  	symbols := make(map[uint64]*CallNode)
    94  	symbolList := make([]*CallNode, 0)
    95  	symbolNames := make(map[string][]*CallNode)
    96  	elfSymbols, err := f.Symbols()
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	for _, elfSymbol := range elfSymbols {
   101  		if elf.ST_TYPE(elfSymbol.Info) != elf.STT_FUNC {
   102  			continue
   103  		}
   104  		address := elfSymbol.Value
   105  		if f.Machine == elf.EM_ARM {
   106  			address = address &^ 1
   107  		}
   108  		var node *CallNode
   109  		if n, ok := symbols[address]; ok {
   110  			// Existing symbol.
   111  			if n.Size != elfSymbol.Size {
   112  				return nil, fmt.Errorf("symbol at 0x%x has inconsistent size (%d for %s and %d for %s)", address, n.Size, n.Names[0], elfSymbol.Size, elfSymbol.Name)
   113  			}
   114  			node = n
   115  			node.Names = append(node.Names, elfSymbol.Name)
   116  		} else {
   117  			// New symbol.
   118  			node = &CallNode{
   119  				Names:   []string{elfSymbol.Name},
   120  				Address: address,
   121  				Size:    elfSymbol.Size,
   122  			}
   123  			symbols[address] = node
   124  			symbolList = append(symbolList, node)
   125  		}
   126  		symbolNames[elfSymbol.Name] = append(symbolNames[elfSymbol.Name], node)
   127  	}
   128  
   129  	// Sort symbols by address, for binary searching.
   130  	sort.Slice(symbolList, func(i, j int) bool {
   131  		return symbolList[i].Address < symbolList[j].Address
   132  	})
   133  
   134  	// Load relocations and construct the call graph.
   135  	for _, section := range f.Sections {
   136  		if section.Type != elf.SHT_REL {
   137  			continue
   138  		}
   139  		if section.Entsize != 8 {
   140  			// Assume ELF32, this should be fixed.
   141  			return nil, fmt.Errorf("only ELF32 is supported at this time")
   142  		}
   143  		data, err := section.Data()
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		for i := uint64(0); i < section.Size/section.Entsize; i++ {
   148  			offset := binary.LittleEndian.Uint32(data[i*section.Entsize:])
   149  			info := binary.LittleEndian.Uint32(data[i*section.Entsize+4:])
   150  			if elf.R_SYM32(info) == 0 {
   151  				continue
   152  			}
   153  			elfSymbol := elfSymbols[elf.R_SYM32(info)-1]
   154  			if elf.ST_TYPE(elfSymbol.Info) != elf.STT_FUNC {
   155  				continue
   156  			}
   157  			address := elfSymbol.Value
   158  			if f.Machine == elf.EM_ARM {
   159  				address = address &^ 1
   160  			}
   161  			childSym := symbols[address]
   162  			switch f.Machine {
   163  			case elf.EM_ARM:
   164  				relocType := elf.R_ARM(elf.R_TYPE32(info))
   165  				parentSym := findSymbol(symbolList, uint64(offset))
   166  				if debugPrint {
   167  					fmt.Fprintf(os.Stderr, "found relocation %-24s at %s (0x%x) to %s (0x%x)\n", relocType, parentSym, offset, childSym, childSym.Address)
   168  				}
   169  				isCall := true
   170  				switch relocType {
   171  				case elf.R_ARM_THM_PC22: // actually R_ARM_THM_CALL
   172  					// used for bl calls
   173  				case elf.R_ARM_THM_JUMP24:
   174  					// used for b.w jumps
   175  					isCall = parentSym != childSym
   176  				case elf.R_ARM_THM_JUMP11:
   177  					// used for b.n jumps
   178  					isCall = parentSym != childSym
   179  				case elf.R_ARM_THM_MOVW_ABS_NC, elf.R_ARM_THM_MOVT_ABS:
   180  					// used for getting a function pointer
   181  					isCall = false
   182  				case elf.R_ARM_ABS32:
   183  					// when compiling with -Oz (minsize), used for calling
   184  					isCall = true
   185  				default:
   186  					return nil, fmt.Errorf("unknown relocation: %s", relocType)
   187  				}
   188  				if isCall {
   189  					if parentSym != nil {
   190  						parentSym.Children = append(parentSym.Children, childSym)
   191  					}
   192  				}
   193  			default:
   194  				return nil, fmt.Errorf("unknown architecture: %s", f.Machine)
   195  			}
   196  		}
   197  	}
   198  
   199  	// Set fixed frame size information, depending on the architecture.
   200  	switch f.Machine {
   201  	case elf.EM_ARM:
   202  		knownFrameSizes := map[string]uint64{
   203  			// implemented with assembly in compiler-rt
   204  			"__aeabi_idivmod":  3 * 4, // 3 registers on thumb1 but 1 register on thumb2
   205  			"__aeabi_uidivmod": 3 * 4, // 3 registers on thumb1 but 1 register on thumb2
   206  			"__aeabi_ldivmod":  2 * 4,
   207  			"__aeabi_uldivmod": 2 * 4,
   208  			"__aeabi_memclr":   2 * 4, // 2 registers on thumb1
   209  			"__aeabi_memset":   2 * 4, // 2 registers on thumb1
   210  			"__aeabi_memcmp":   2 * 4, // 2 registers on thumb1
   211  			"__aeabi_memcpy":   2 * 4, // 2 registers on thumb1
   212  			"__aeabi_memmove":  2 * 4, // 2 registers on thumb1
   213  			"__aeabi_dcmpeq":   2 * 4,
   214  			"__aeabi_dcmplt":   2 * 4,
   215  			"__aeabi_dcmple":   2 * 4,
   216  			"__aeabi_dcmpge":   2 * 4,
   217  			"__aeabi_dcmpgt":   2 * 4,
   218  			"__aeabi_fcmpeq":   2 * 4,
   219  			"__aeabi_fcmplt":   2 * 4,
   220  			"__aeabi_fcmple":   2 * 4,
   221  			"__aeabi_fcmpge":   2 * 4,
   222  			"__aeabi_fcmpgt":   2 * 4,
   223  		}
   224  		for name, size := range knownFrameSizes {
   225  			if sym, ok := symbolNames[name]; ok {
   226  				if len(sym) > 1 {
   227  					return nil, fmt.Errorf("expected zero or one occurence of the symbol %s, found %d", name, len(sym))
   228  				}
   229  				sym[0].FrameSize = size
   230  				sym[0].FrameSizeType = Bounded
   231  			}
   232  		}
   233  	}
   234  
   235  	// Mark functions that do indirect calls (which cannot be determined
   236  	// directly from ELF/DWARF information).
   237  	for _, name := range callsIndirectFunction {
   238  		for _, fn := range symbolNames[name] {
   239  			fn.stackSizeType = IndirectCall
   240  			fn.missingFrameInfo = fn
   241  		}
   242  	}
   243  
   244  	// Read the .debug_frame section.
   245  	section := f.Section(".debug_frame")
   246  	if section == nil {
   247  		return nil, errors.New("no .debug_frame section present, binary was compiled without debug information")
   248  	}
   249  	data, err := section.Data()
   250  	if err != nil {
   251  		return nil, fmt.Errorf("could not read .debug_frame section: %w", err)
   252  	}
   253  	err = parseFrames(f, data, symbols)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	return symbolNames, nil
   259  }
   260  
   261  // findSymbol determines in which symbol the given address lies.
   262  func findSymbol(symbolList []*CallNode, address uint64) *CallNode {
   263  	// TODO: binary search
   264  	for _, sym := range symbolList {
   265  		if address >= sym.Address && address < sym.Address+sym.Size {
   266  			return sym
   267  		}
   268  	}
   269  	return nil
   270  }
   271  
   272  // StackSize tries to determine the stack size of the given call graph node. It
   273  // returns the maximum stack size, whether this size can be known at compile
   274  // time and the call node responsible for failing to determine the maximum stack
   275  // usage. The stack size is only valid if sizeType is Bounded.
   276  func (node *CallNode) StackSize() (uint64, SizeType, *CallNode) {
   277  	if node.stackSizeType == Undefined {
   278  		node.determineStackSize(make(map[*CallNode]struct{}))
   279  	}
   280  	return node.stackSize, node.stackSizeType, node.missingFrameInfo
   281  }
   282  
   283  // determineStackSize tries to determine the maximum stack size for this
   284  // function, recursively.
   285  func (node *CallNode) determineStackSize(parents map[*CallNode]struct{}) {
   286  	if _, ok := parents[node]; ok {
   287  		// The function calls itself (directly or indirectly).
   288  		node.stackSizeType = Recursive
   289  		node.missingFrameInfo = node
   290  		return
   291  	}
   292  	parents[node] = struct{}{}
   293  	defer func() {
   294  		delete(parents, node)
   295  	}()
   296  	switch node.FrameSizeType {
   297  	case Bounded:
   298  		// Determine the stack size recursively.
   299  		childMaxStackSize := uint64(0)
   300  		for _, child := range node.Children {
   301  			if child.stackSizeType == Undefined {
   302  				child.determineStackSize(parents)
   303  			}
   304  			switch child.stackSizeType {
   305  			case Bounded:
   306  				if child.stackSize > childMaxStackSize {
   307  					childMaxStackSize = child.stackSize
   308  				}
   309  			case Unknown, Recursive, IndirectCall:
   310  				node.stackSizeType = child.stackSizeType
   311  				node.missingFrameInfo = child.missingFrameInfo
   312  				return
   313  			default:
   314  				panic("unknown child stack size type")
   315  			}
   316  		}
   317  		node.stackSize = node.FrameSize + childMaxStackSize
   318  		node.stackSizeType = Bounded
   319  	case Undefined:
   320  		node.stackSizeType = Unknown
   321  		node.missingFrameInfo = node
   322  	default:
   323  		panic("unknown frame size type") // unreachable
   324  	}
   325  }