mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-11 22:43:40 +00:00
arm: automatically determine stack sizes
This is a big change that will determine the stack size for many goroutines automatically. Functions that aren't recursive and don't call function pointers can in many cases have an automatically determined worst case stack size. This is useful, as the stack size is usually much lower than the previous hardcoded default of 1024 bytes: somewhere around 200-500 bytes is common. A side effect of this change is that the default stack sizes (including the stack size for other architectures such as AVR) can now be changed in the config JSON file, making it tunable per application.
This commit is contained in:
committed by
Ron Evans
parent
a761f556ff
commit
a21a039ac7
+198
-42
@@ -5,6 +5,7 @@ package builder
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -115,6 +116,13 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure stack sizes are loaded from a separate section so they can be
|
||||
// modified after linking.
|
||||
var stackSizeLoads []string
|
||||
if config.AutomaticStackSize() {
|
||||
stackSizeLoads = transform.CreateStackSizeLoads(mod, config)
|
||||
}
|
||||
|
||||
// Generate output.
|
||||
outext := filepath.Ext(outpath)
|
||||
switch outext {
|
||||
@@ -207,6 +215,26 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
|
||||
return &commandError{"failed to link", executable, err}
|
||||
}
|
||||
|
||||
var calculatedStacks []string
|
||||
var stackSizes map[string]functionStackSize
|
||||
if config.Options.PrintStacks || config.AutomaticStackSize() {
|
||||
// Try to determine stack sizes at compile time.
|
||||
// Don't do this by default as it usually doesn't work on
|
||||
// unsupported architectures.
|
||||
calculatedStacks, stackSizes, err = determineStackSizes(mod, executable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if config.AutomaticStackSize() {
|
||||
// Modify the .tinygo_stacksizes section that contains a stack size
|
||||
// for each goroutine.
|
||||
err = modifyStackSizes(executable, stackSizeLoads, stackSizes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not modify stack sizes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" {
|
||||
sizes, err := loadProgramSize(executable)
|
||||
if err != nil {
|
||||
@@ -228,7 +256,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
|
||||
|
||||
// Print goroutine stack sizes, as far as possible.
|
||||
if config.Options.PrintStacks {
|
||||
printStacks(mod, executable)
|
||||
printStacks(calculatedStacks, stackSizes)
|
||||
}
|
||||
|
||||
// Get an Intel .hex file or .bin file from the .elf file.
|
||||
@@ -250,19 +278,19 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
|
||||
}
|
||||
}
|
||||
|
||||
// printStacks prints the maximum stack depth for functions that are started as
|
||||
// goroutines. Stack sizes cannot always be determined statically, in particular
|
||||
// recursive functions and functions that call interface methods or function
|
||||
// pointers may have an unknown stack depth (depending on what the optimizer
|
||||
// manages to optimize away).
|
||||
//
|
||||
// It might print something like the following:
|
||||
//
|
||||
// function stack usage (in bytes)
|
||||
// Reset_Handler 316
|
||||
// examples/blinky2.led1 92
|
||||
// runtime.run$1 300
|
||||
func printStacks(mod llvm.Module, executable string) {
|
||||
// functionStackSizes keeps stack size information about a single function
|
||||
// (usually a goroutine).
|
||||
type functionStackSize struct {
|
||||
humanName string
|
||||
stackSize uint64
|
||||
stackSizeType stacksize.SizeType
|
||||
missingStackSize *stacksize.CallNode
|
||||
}
|
||||
|
||||
// determineStackSizes tries to determine the stack sizes of all started
|
||||
// goroutines and of the reset vector. The LLVM module is necessary to find
|
||||
// functions that call a function pointer.
|
||||
func determineStackSizes(mod llvm.Module, executable string) ([]string, map[string]functionStackSize, error) {
|
||||
var callsIndirectFunction []string
|
||||
gowrappers := []string{}
|
||||
gowrapperNames := make(map[string]string)
|
||||
@@ -292,48 +320,176 @@ func printStacks(mod llvm.Module, executable string) {
|
||||
// Load the ELF binary.
|
||||
f, err := elf.Open(executable)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "could not load executable for stack size analysis:", err)
|
||||
return
|
||||
return nil, nil, fmt.Errorf("could not load executable for stack size analysis: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Determine the frame size of each function (if available) and the callgraph.
|
||||
functions, err := stacksize.CallGraph(f, callsIndirectFunction)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "could not parse executable for stack size analysis:", err)
|
||||
return
|
||||
return nil, nil, fmt.Errorf("could not parse executable for stack size analysis: %w", err)
|
||||
}
|
||||
|
||||
// Goroutines need to be started and finished and take up some stack space
|
||||
// that way. This can be measured by measuing the stack size of
|
||||
// tinygo_startTask.
|
||||
if numFuncs := len(functions["tinygo_startTask"]); numFuncs != 1 {
|
||||
return nil, nil, fmt.Errorf("expected exactly one definition of tinygo_startTask, got %d", numFuncs)
|
||||
}
|
||||
baseStackSize, baseStackSizeType, baseStackSizeFailedAt := functions["tinygo_startTask"][0].StackSize()
|
||||
|
||||
sizes := make(map[string]functionStackSize)
|
||||
|
||||
// Add the reset handler function, for convenience. The reset handler runs
|
||||
// startup code and the scheduler. The listed stack size is not the full
|
||||
// stack size: interrupts are not counted.
|
||||
var resetFunction string
|
||||
switch f.Machine {
|
||||
case elf.EM_ARM:
|
||||
// Add the reset handler, which runs startup code and is the
|
||||
// interrupt/scheduler stack with -scheduler=tasks.
|
||||
// Note that because interrupts happen on this stack, the stack needed
|
||||
// by just the Reset_Handler is not enough. Stacks needed by interrupt
|
||||
// handlers should also be taken into account.
|
||||
gowrappers = append([]string{"Reset_Handler"}, gowrappers...)
|
||||
gowrapperNames["Reset_Handler"] = "Reset_Handler"
|
||||
// Note: all interrupts happen on this stack so the real size is bigger.
|
||||
resetFunction = "Reset_Handler"
|
||||
}
|
||||
if resetFunction != "" {
|
||||
funcs := functions[resetFunction]
|
||||
if len(funcs) != 1 {
|
||||
return nil, nil, fmt.Errorf("expected exactly one definition of %s in the callgraph, found %d", resetFunction, len(funcs))
|
||||
}
|
||||
stackSize, stackSizeType, missingStackSize := funcs[0].StackSize()
|
||||
sizes[resetFunction] = functionStackSize{
|
||||
stackSize: stackSize,
|
||||
stackSizeType: stackSizeType,
|
||||
missingStackSize: missingStackSize,
|
||||
humanName: resetFunction,
|
||||
}
|
||||
}
|
||||
|
||||
// Add all goroutine wrapper functions.
|
||||
for _, name := range gowrappers {
|
||||
funcs := functions[name]
|
||||
if len(funcs) != 1 {
|
||||
return nil, nil, fmt.Errorf("expected exactly one definition of %s in the callgraph, found %d", name, len(funcs))
|
||||
}
|
||||
humanName := gowrapperNames[name]
|
||||
if humanName == "" {
|
||||
humanName = name // fallback
|
||||
}
|
||||
stackSize, stackSizeType, missingStackSize := funcs[0].StackSize()
|
||||
if baseStackSizeType != stacksize.Bounded {
|
||||
// It was not possible to determine the stack size at compile time
|
||||
// because tinygo_startTask does not have a fixed stack size. This
|
||||
// can happen when using -opt=1.
|
||||
stackSizeType = baseStackSizeType
|
||||
missingStackSize = baseStackSizeFailedAt
|
||||
} else if stackSize < baseStackSize {
|
||||
// This goroutine has a very small stack, but still needs to fit all
|
||||
// registers to start and suspend the goroutine. Otherwise a stack
|
||||
// overflow will occur even before the goroutine is started.
|
||||
stackSize = baseStackSize
|
||||
}
|
||||
sizes[name] = functionStackSize{
|
||||
stackSize: stackSize,
|
||||
stackSizeType: stackSizeType,
|
||||
missingStackSize: missingStackSize,
|
||||
humanName: humanName,
|
||||
}
|
||||
}
|
||||
|
||||
if resetFunction != "" {
|
||||
return append([]string{resetFunction}, gowrappers...), sizes, nil
|
||||
}
|
||||
return gowrappers, sizes, nil
|
||||
}
|
||||
|
||||
// modifyStackSizes modifies the .tinygo_stacksizes section with the updated
|
||||
// stack size information. Before this modification, all stack sizes in the
|
||||
// section assume the default stack size (which is relatively big).
|
||||
func modifyStackSizes(executable string, stackSizeLoads []string, stackSizes map[string]functionStackSize) error {
|
||||
fp, err := os.OpenFile(executable, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
elfFile, err := elf.NewFile(fp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
section := elfFile.Section(".tinygo_stacksizes")
|
||||
if section == nil {
|
||||
return errors.New("could not find .tinygo_stacksizes section")
|
||||
}
|
||||
|
||||
if section.Size != section.FileSize {
|
||||
// Sanity check.
|
||||
return fmt.Errorf("expected .tinygo_stacksizes to have identical size and file size, got %d and %d", section.Size, section.FileSize)
|
||||
}
|
||||
|
||||
// Read all goroutine stack sizes.
|
||||
data := make([]byte, section.Size)
|
||||
_, err = fp.ReadAt(data, int64(section.Offset))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(stackSizeLoads)*4 != len(data) {
|
||||
// Note: while AVR should use 2 byte stack sizes, even 64-bit platforms
|
||||
// should probably stick to 4 byte stack sizes as a larger than 4GB
|
||||
// stack doesn't make much sense.
|
||||
return errors.New("expected 4 byte stack sizes")
|
||||
}
|
||||
|
||||
// Modify goroutine stack sizes with a compile-time known worst case stack
|
||||
// size.
|
||||
for i, name := range stackSizeLoads {
|
||||
fn, ok := stackSizes[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("could not find symbol %s in ELF file", name)
|
||||
}
|
||||
if fn.stackSizeType == stacksize.Bounded {
|
||||
// Note: adding 4 for the stack canary. Even though the size may be
|
||||
// automatically determined, stack overflow checking is still
|
||||
// important as the stack size cannot be determined for all
|
||||
// goroutines.
|
||||
binary.LittleEndian.PutUint32(data[i*4:], uint32(fn.stackSize)+4)
|
||||
}
|
||||
}
|
||||
|
||||
// Write back the modified stack sizes.
|
||||
_, err = fp.WriteAt(data, int64(section.Offset))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printStacks prints the maximum stack depth for functions that are started as
|
||||
// goroutines. Stack sizes cannot always be determined statically, in particular
|
||||
// recursive functions and functions that call interface methods or function
|
||||
// pointers may have an unknown stack depth (depending on what the optimizer
|
||||
// manages to optimize away).
|
||||
//
|
||||
// It might print something like the following:
|
||||
//
|
||||
// function stack usage (in bytes)
|
||||
// Reset_Handler 316
|
||||
// examples/blinky2.led1 92
|
||||
// runtime.run$1 300
|
||||
func printStacks(calculatedStacks []string, stackSizes map[string]functionStackSize) {
|
||||
// Print the sizes of all stacks.
|
||||
fmt.Printf("%-32s %s\n", "function", "stack usage (in bytes)")
|
||||
for _, name := range gowrappers {
|
||||
for _, fn := range functions[name] {
|
||||
stackSize, stackSizeType, missingStackSize := fn.StackSize()
|
||||
funcName := gowrapperNames[name]
|
||||
if funcName == "" {
|
||||
funcName = "<unknown>"
|
||||
}
|
||||
switch stackSizeType {
|
||||
case stacksize.Bounded:
|
||||
fmt.Printf("%-32s %d\n", funcName, stackSize)
|
||||
case stacksize.Unknown:
|
||||
fmt.Printf("%-32s unknown, %s does not have stack frame information\n", funcName, missingStackSize)
|
||||
case stacksize.Recursive:
|
||||
fmt.Printf("%-32s recursive, %s may call itself\n", funcName, missingStackSize)
|
||||
case stacksize.IndirectCall:
|
||||
fmt.Printf("%-32s unknown, %s calls a function pointer\n", funcName, missingStackSize)
|
||||
}
|
||||
for _, name := range calculatedStacks {
|
||||
fn := stackSizes[name]
|
||||
switch fn.stackSizeType {
|
||||
case stacksize.Bounded:
|
||||
fmt.Printf("%-32s %d\n", fn.humanName, fn.stackSize)
|
||||
case stacksize.Unknown:
|
||||
fmt.Printf("%-32s unknown, %s does not have stack frame information\n", fn.humanName, fn.missingStackSize)
|
||||
case stacksize.Recursive:
|
||||
fmt.Printf("%-32s recursive, %s may call itself\n", fn.humanName, fn.missingStackSize)
|
||||
case stacksize.IndirectCall:
|
||||
fmt.Printf("%-32s unknown, %s calls a function pointer\n", fn.humanName, fn.missingStackSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user