From a572b54d9a70214305ab3520197e8d66776efc32 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 26 Apr 2023 23:14:50 +0200 Subject: [PATCH] cortexm: optimize code size for the HardFault_Handler 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). --- builder/sizes_test.go | 4 +- src/device/arm/cortexm.S | 23 ----------- src/runtime/panic.go | 3 ++ src/runtime/runtime.go | 6 +++ src/runtime/runtime_cortexm_hardfault.go | 40 ++++++------------- .../runtime_cortexm_hardfault_debug.go | 9 ++++- 6 files changed, 31 insertions(+), 54 deletions(-) diff --git a/builder/sizes_test.go b/builder/sizes_test.go index c89c07588b..f786d9c826 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -43,8 +43,8 @@ func TestBinarySize(t *testing.T) { tests := []sizeTest{ // microcontrollers {"hifive1b", "examples/echo", 3884, 280, 0, 2268}, - {"microbit", "examples/serial", 2924, 388, 8, 2272}, - {"wioterminal", "examples/pininterrupt", 7365, 1491, 116, 6912}, + {"microbit", "examples/serial", 2852, 360, 8, 2272}, + {"wioterminal", "examples/pininterrupt", 7337, 1491, 116, 6912}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/src/device/arm/cortexm.S b/src/device/arm/cortexm.S index e9b15aafe1..22b8357b63 100644 --- a/src/device/arm/cortexm.S +++ b/src/device/arm/cortexm.S @@ -1,29 +1,6 @@ .syntax unified .cfi_sections .debug_frame -.section .text.HardFault_Handler -.global HardFault_Handler -.type HardFault_Handler, %function -HardFault_Handler: - .cfi_startproc - // Put the old stack pointer in the first argument, for easy debugging. This - // is especially useful on Cortex-M0, which supports far fewer debug - // facilities. - mov r0, sp - - // Load the default stack pointer from address 0 so that we can call normal - // functions again that expect a working stack. However, it will corrupt the - // old stack so the function below must not attempt to recover from this - // fault. - movs r3, #0 - ldr r3, [r3] - mov sp, r3 - - // Continue handling this error in Go. - bl handleHardFault - .cfi_endproc -.size HardFault_Handler, .-HardFault_Handler - // This is a convenience function for semihosting support. // At some point, this should be replaced by inline assembly. .section .text.SemihostingCall diff --git a/src/runtime/panic.go b/src/runtime/panic.go index e18a306349..5eac60ecd9 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -93,6 +93,9 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) { 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(": ") diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 3bd98319e8..c9b0959384 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -57,6 +57,12 @@ func memzero(ptr unsafe.Pointer, size uintptr) // the current stack pointer in a platform-independent way. func stacksave() unsafe.Pointer +// Special LLVM intrinsic that returns the SP register on entry to the calling +// function. +// +//export llvm.sponentry.p0 +func llvm_sponentry() unsafe.Pointer + //export strlen func strlen(ptr unsafe.Pointer) uintptr diff --git a/src/runtime/runtime_cortexm_hardfault.go b/src/runtime/runtime_cortexm_hardfault.go index b2449ed910..a1cfc2c3d6 100644 --- a/src/runtime/runtime_cortexm_hardfault.go +++ b/src/runtime/runtime_cortexm_hardfault.go @@ -2,39 +2,25 @@ package runtime -import ( - "unsafe" -) - // This function is called at HardFault. -// Before this function is called, the stack pointer is reset to the initial -// stack pointer (loaded from address 0x0) and the previous stack pointer is -// passed as an argument to this function. This allows for easy inspection of -// the stack the moment a HardFault occurs, but it means that the stack will be -// corrupted by this function and thus this handler must not attempt to recover. // // For details, see: // https://community.arm.com/developer/ip-products/system/f/embedded-forum/3257/debugging-a-cortex-m0-hard-fault // https://blog.feabhas.com/2013/02/developing-a-generic-hard-fault-handler-for-arm-cortex-m3cortex-m4/ // -//export handleHardFault -func handleHardFault(sp *interruptStack) { - print("fatal error: ") - if uintptr(unsafe.Pointer(sp)) < 0x20000000 { - print("stack overflow") - } else { - // TODO: try to find the cause of the hard fault. Especially on - // Cortex-M3 and higher it is possible to find more detailed information - // in special status registers. - print("HardFault") - } - print(" with sp=", sp) - if uintptr(unsafe.Pointer(&sp.PC)) >= 0x20000000 { - // Only print the PC if it points into memory. - // It may not point into memory during a stack overflow, so check that - // first before accessing the stack. - print(" pc=", sp.PC) - } +//export HardFault_Handler +func HardFault_Handler() { + // Obtain the stack pointer as it was on entry to the HardFault. It contains + // the registers that were pushed by the NVIC and that we can now read back + // to print the PC value at the time of the hard fault, for example. + sp := (*interruptStack)(llvm_sponentry()) + + // Note: by reusing the string "panic: runtime error at " we save a little + // bit in terms of code size as the string can be deduplicated. + print("panic: runtime error at ", sp.PC, ": HardFault with sp=", sp) + // TODO: try to find the cause of the hard fault. Especially on Cortex-M3 + // and higher it is possible to find more detailed information in special + // status registers. println() abort() } diff --git a/src/runtime/runtime_cortexm_hardfault_debug.go b/src/runtime/runtime_cortexm_hardfault_debug.go index ea6c507028..7e953a67de 100644 --- a/src/runtime/runtime_cortexm_hardfault_debug.go +++ b/src/runtime/runtime_cortexm_hardfault_debug.go @@ -18,8 +18,13 @@ const ( // See runtime_cortexm_hardfault.go // -//go:export handleHardFault -func handleHardFault(sp *interruptStack) { +//export HardFault_Handler +func HardFault_Handler() { + // Obtain the stack pointer as it was on entry to the HardFault. It contains + // the registers that were pushed by the NVIC and that we can now read back + // to print the PC value at the time of the hard fault, for example. + sp := (*interruptStack)(llvm_sponentry()) + fault := GetFaultStatus() spValid := !fault.Bus().ImpreciseDataBusError()