mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-03 10:37:46 +00:00
0a87846bd8
This function is called when a hard fault occurs. Hard faults happen
when something really bad happens - like writing to unwritable memory or
an unaligned memory access on Cortex-M0. It is not generally possible to
recover from these.
This commit optimizes the code size overhead of hard fault handling:
* It removes the stack overflow checking code.
This may seem like a bad thing, but the only thing this could check
were stack overflows outside goroutines. In practice, this could
only really happen on a stack overflow in the scheduler (unlikely),
or in interrupt code (possible, but interrupts are small so still
unlikely). Most stack overflows happen in regular goroutines, and
weren't caught in the HardFault.
* It makes the panic message similar to a regular panic. This has two
advantages:
* It reduces code size, because the string can be reused between
the HardFault handler and the runtime panic function.
* Using the same pattern automatically makes `-monitor` print the
source address for the hard fault. Not a big benefit as we could
trivially add any other pattern but a nice benefit nonetheless.
Result:
$ tinygo flash -target=microbit -size=short -programmer=openocd -monitor examples/serial
code data bss | flash ram
3036 8 2256 | 3044 2264
[...snip]
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
panic: runtime error at 0x00000344: HardFault with sp=0x200007d0
[tinygo: panic at /home/ayke/src/tinygo/tinygo/src/internal/task/task_stack_cortexm.go:48:4]
(This is with https://github.com/tinygo-org/tinygo/pull/3680 not yet
fixed and some local changes to configure the UART so I can actually see
the panic).
For atsamd21/nrf51 chips this results in a binary size reduction of
around 100 bytes. For other Cortex-M chips it's around 24 bytes but I
hope to change this in the future because a lot of the fault decoding in
runtime_cortexm_hardfault_debug.go should IMHO be done by the TinyGo
monitor instead (I estimate that this would save around 800 bytes on
these chips).
238 lines
7.5 KiB
Go
238 lines
7.5 KiB
Go
package runtime
|
|
|
|
import (
|
|
"internal/task"
|
|
"runtime/interrupt"
|
|
"tinygo"
|
|
"unsafe"
|
|
)
|
|
|
|
// trap is a compiler hint that this function cannot be executed. It is
|
|
// translated into either a trap instruction or a call to abort().
|
|
//
|
|
//export llvm.trap
|
|
func trap()
|
|
|
|
// Inline assembly stub. It is essentially C longjmp but modified a bit for the
|
|
// purposes of TinyGo. It restores the stack pointer and jumps to the given pc.
|
|
//
|
|
//export tinygo_longjmp
|
|
func tinygo_longjmp(frame *deferFrame)
|
|
|
|
// Compiler intrinsic.
|
|
// Returns whether recover is supported on the current architecture.
|
|
func supportsRecover() bool
|
|
|
|
// Compile intrinsic.
|
|
// Returns which strategy is used. This is usually "print" but can be changed
|
|
// using the -panic= compiler flag.
|
|
func panicStrategy() uint8
|
|
|
|
// DeferFrame is a stack allocated object that stores information for the
|
|
// current "defer frame", which is used in functions that use the `defer`
|
|
// keyword.
|
|
// The compiler knows about the JumpPC struct offset, so it should not be moved
|
|
// without also updating compiler/defer.go.
|
|
type deferFrame struct {
|
|
JumpSP unsafe.Pointer // stack pointer to return to
|
|
JumpPC unsafe.Pointer // pc to return to
|
|
ExtraRegs [deferExtraRegs]unsafe.Pointer // extra registers (depending on the architecture)
|
|
Previous *deferFrame // previous recover buffer pointer
|
|
Panicking panicState // not panicking, panicking, or in Goexit
|
|
PanicValue interface{} // panic value, might be nil for panic(nil) for example
|
|
}
|
|
|
|
type panicState uint8
|
|
|
|
const (
|
|
panicFalse panicState = iota
|
|
panicTrue
|
|
panicGoexit
|
|
)
|
|
|
|
// Builtin function panic(msg), used as a compiler intrinsic.
|
|
func _panic(message interface{}) {
|
|
panicOrGoexit(message, panicTrue)
|
|
}
|
|
|
|
func panicOrGoexit(message interface{}, panicking panicState) {
|
|
if panicStrategy() == tinygo.PanicStrategyTrap {
|
|
trap()
|
|
}
|
|
// Note: recover is not supported inside interrupts.
|
|
// (This could be supported, like defer, but we currently don't).
|
|
if supportsRecover() && !interrupt.In() {
|
|
frame := (*deferFrame)(task.Current().DeferFrame)
|
|
if frame != nil {
|
|
frame.PanicValue = message
|
|
frame.Panicking = panicking
|
|
tinygo_longjmp(frame)
|
|
// unreachable
|
|
}
|
|
}
|
|
if panicking == panicGoexit {
|
|
// Call to Goexit() instead of a panic.
|
|
// Exit the goroutine instead of printing a panic message.
|
|
deadlock()
|
|
}
|
|
printstring("panic: ")
|
|
printitf(message)
|
|
printnl()
|
|
abort()
|
|
}
|
|
|
|
// Cause a runtime panic, which is (currently) always a string.
|
|
func runtimePanic(msg string) {
|
|
// As long as this function is inined, llvm.returnaddress(0) will return
|
|
// something sensible.
|
|
runtimePanicAt(returnAddress(0), msg)
|
|
}
|
|
|
|
func runtimePanicAt(addr unsafe.Pointer, msg string) {
|
|
if panicStrategy() == tinygo.PanicStrategyTrap {
|
|
trap()
|
|
}
|
|
if hasReturnAddr {
|
|
// Note: the string "panic: runtime error at " is also used in
|
|
// runtime_cortexm_hardfault.go. It is kept the same so that the string
|
|
// can be deduplicated by the compiler.
|
|
printstring("panic: runtime error at ")
|
|
printptr(uintptr(addr) - callInstSize)
|
|
printstring(": ")
|
|
} else {
|
|
printstring("panic: runtime error: ")
|
|
}
|
|
printstring(msg)
|
|
printnl()
|
|
abort()
|
|
}
|
|
|
|
// Called at the start of a function that includes a deferred call.
|
|
// It gets passed in the stack-allocated defer frame and configures it.
|
|
// Note that the frame is not zeroed yet, so we need to initialize all values
|
|
// that will be used.
|
|
//
|
|
//go:inline
|
|
//go:nobounds
|
|
func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) {
|
|
if interrupt.In() {
|
|
// Defer is not currently allowed in interrupts.
|
|
// We could add support for this, but since defer might also allocate
|
|
// (especially in loops) it might not be a good idea anyway.
|
|
runtimePanicAt(returnAddress(0), "defer in interrupt")
|
|
}
|
|
currentTask := task.Current()
|
|
frame.Previous = (*deferFrame)(currentTask.DeferFrame)
|
|
frame.JumpSP = jumpSP
|
|
frame.Panicking = panicFalse
|
|
currentTask.DeferFrame = unsafe.Pointer(frame)
|
|
}
|
|
|
|
// Called right before the return instruction. It pops the defer frame from the
|
|
// linked list of defer frames. It also re-raises a panic if the goroutine is
|
|
// still panicking.
|
|
//
|
|
//go:inline
|
|
//go:nobounds
|
|
func destroyDeferFrame(frame *deferFrame) {
|
|
task.Current().DeferFrame = unsafe.Pointer(frame.Previous)
|
|
if frame.Panicking != panicFalse {
|
|
// We're still panicking!
|
|
// Re-raise the panic now.
|
|
panicOrGoexit(frame.PanicValue, frame.Panicking)
|
|
}
|
|
}
|
|
|
|
// _recover is the built-in recover() function. It tries to recover a currently
|
|
// panicking goroutine.
|
|
// useParentFrame is set when the caller of runtime._recover has a defer frame
|
|
// itself. In that case, recover() shouldn't check that frame but one frame up.
|
|
func _recover(useParentFrame bool) interface{} {
|
|
if !supportsRecover() || interrupt.In() {
|
|
// Either we're compiling without stack unwinding support, or we're
|
|
// inside an interrupt where panic/recover is not supported. Either way,
|
|
// make this a no-op since panic() won't do any long jumps to a deferred
|
|
// function.
|
|
return nil
|
|
}
|
|
// TODO: somehow check that recover() is called directly by a deferred
|
|
// function in a panicking goroutine. Maybe this can be done by comparing
|
|
// the frame pointer?
|
|
frame := (*deferFrame)(task.Current().DeferFrame)
|
|
if useParentFrame {
|
|
// Don't recover panic from the current frame (which can't be panicking
|
|
// already), but instead from the previous frame.
|
|
frame = frame.Previous
|
|
}
|
|
if frame != nil && frame.Panicking != panicFalse {
|
|
if frame.Panicking == panicGoexit {
|
|
// Special value that indicates we're exiting the goroutine using
|
|
// Goexit(). Therefore, make this recover call a no-op.
|
|
return nil
|
|
}
|
|
// Only the first call to recover returns the panic value. It also stops
|
|
// the panicking sequence, hence setting panicking to false.
|
|
frame.Panicking = panicFalse
|
|
return frame.PanicValue
|
|
}
|
|
// Not panicking, so return a nil interface.
|
|
return nil
|
|
}
|
|
|
|
// Panic when trying to dereference a nil pointer.
|
|
func nilPanic() {
|
|
runtimePanicAt(returnAddress(0), "nil pointer dereference")
|
|
}
|
|
|
|
// Panic when trying to add an entry to a nil map
|
|
func nilMapPanic() {
|
|
runtimePanicAt(returnAddress(0), "assignment to entry in nil map")
|
|
}
|
|
|
|
// Panic when trying to access an array or slice out of bounds.
|
|
func lookupPanic() {
|
|
runtimePanicAt(returnAddress(0), "index out of range")
|
|
}
|
|
|
|
// Panic when trying to slice a slice out of bounds.
|
|
func slicePanic() {
|
|
runtimePanicAt(returnAddress(0), "slice out of range")
|
|
}
|
|
|
|
// Panic when trying to convert a slice to an array pointer (Go 1.17+) and the
|
|
// slice is shorter than the array.
|
|
func sliceToArrayPointerPanic() {
|
|
runtimePanicAt(returnAddress(0), "slice smaller than array")
|
|
}
|
|
|
|
// Panic when calling unsafe.Slice() (Go 1.17+) or unsafe.String() (Go 1.20+)
|
|
// with a len that's too large (which includes if the ptr is nil and len is
|
|
// nonzero).
|
|
func unsafeSlicePanic() {
|
|
runtimePanicAt(returnAddress(0), "unsafe.Slice/String: len out of range")
|
|
}
|
|
|
|
// Panic when trying to create a new channel that is too big.
|
|
func chanMakePanic() {
|
|
runtimePanicAt(returnAddress(0), "new channel is too big")
|
|
}
|
|
|
|
// Panic when a shift value is negative.
|
|
func negativeShiftPanic() {
|
|
runtimePanicAt(returnAddress(0), "negative shift")
|
|
}
|
|
|
|
// Panic when there is a divide by zero.
|
|
func divideByZeroPanic() {
|
|
runtimePanicAt(returnAddress(0), "divide by zero")
|
|
}
|
|
|
|
func blockingPanic() {
|
|
runtimePanicAt(returnAddress(0), "trying to do blocking operation in exported function")
|
|
}
|
|
|
|
//go:linkname fips_fatal crypto/internal/fips140.fatal
|
|
func fips_fatal(msg string) {
|
|
runtimePanic(msg)
|
|
}
|