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 }