From 3392827c3e6ac2c0849e443242576c1dd24f6677 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 25 Apr 2023 01:22:55 +0200 Subject: [PATCH] runtime: print the address where a panic happened This is not very useful in itself, but makes it possible to detect this address in the output. See the next commit. This adds around 50 bytes to each binary (except for AVR and wasm). This is unfortunate, but I think this feature is quite useful still. A future enhancement might be to create a build tag for extended panic information that's not set by default. --- builder/sizes_test.go | 6 +++--- src/runtime/arch-has-returnaddr.go | 10 +++++++++ src/runtime/arch-no-returnaddr.go | 11 ++++++++++ src/runtime/arch_386.go | 2 ++ src/runtime/arch_amd64.go | 2 ++ src/runtime/arch_arm.go | 2 ++ src/runtime/arch_arm64.go | 2 ++ src/runtime/arch_avr.go | 2 ++ src/runtime/arch_cortexm.go | 2 ++ src/runtime/arch_tinygoriscv.go | 2 ++ src/runtime/arch_tinygowasm.go | 2 ++ src/runtime/arch_xtensa.go | 2 ++ src/runtime/gc_blocks.go | 4 ++-- src/runtime/panic.go | 34 ++++++++++++++++++++---------- 14 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 src/runtime/arch-has-returnaddr.go create mode 100644 src/runtime/arch-no-returnaddr.go diff --git a/builder/sizes_test.go b/builder/sizes_test.go index e935b6ec..b987ba19 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -41,9 +41,9 @@ func TestBinarySize(t *testing.T) { // This is a small number of very diverse targets that we want to test. tests := []sizeTest{ // microcontrollers - {"hifive1b", "examples/echo", 4556, 272, 0, 2252}, - {"microbit", "examples/serial", 2680, 380, 8, 2256}, - {"wioterminal", "examples/pininterrupt", 6109, 1471, 116, 6816}, + {"hifive1b", "examples/echo", 4612, 276, 0, 2252}, + {"microbit", "examples/serial", 2724, 384, 8, 2256}, + {"wioterminal", "examples/pininterrupt", 6159, 1477, 116, 6816}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/src/runtime/arch-has-returnaddr.go b/src/runtime/arch-has-returnaddr.go new file mode 100644 index 00000000..a356d3c1 --- /dev/null +++ b/src/runtime/arch-has-returnaddr.go @@ -0,0 +1,10 @@ +//go:build !(avr || tinygo.wasm) + +package runtime + +import "unsafe" + +const hasReturnAddr = true + +//export llvm.returnaddress +func returnAddress(level uint32) unsafe.Pointer diff --git a/src/runtime/arch-no-returnaddr.go b/src/runtime/arch-no-returnaddr.go new file mode 100644 index 00000000..881eb2df --- /dev/null +++ b/src/runtime/arch-no-returnaddr.go @@ -0,0 +1,11 @@ +//go:build avr || tinygo.wasm + +package runtime + +import "unsafe" + +const hasReturnAddr = false + +func returnAddress(level uint32) unsafe.Pointer { + return nil +} diff --git a/src/runtime/arch_386.go b/src/runtime/arch_386.go index 14eda3c5..c4dbdf16 100644 --- a/src/runtime/arch_386.go +++ b/src/runtime/arch_386.go @@ -7,6 +7,8 @@ const TargetBits = 32 const deferExtraRegs = 0 +const callInstSize = 5 // "call someFunction" is 5 bytes + // Align on word boundary. func align(ptr uintptr) uintptr { return (ptr + 15) &^ 15 diff --git a/src/runtime/arch_amd64.go b/src/runtime/arch_amd64.go index b9ac5104..99647601 100644 --- a/src/runtime/arch_amd64.go +++ b/src/runtime/arch_amd64.go @@ -7,6 +7,8 @@ const TargetBits = 64 const deferExtraRegs = 0 +const callInstSize = 5 // "call someFunction" is 5 bytes + // Align a pointer. // Note that some amd64 instructions (like movaps) expect 16-byte aligned // memory, thus the result must be 16-byte aligned. diff --git a/src/runtime/arch_arm.go b/src/runtime/arch_arm.go index 1868b7eb..33a7513b 100644 --- a/src/runtime/arch_arm.go +++ b/src/runtime/arch_arm.go @@ -9,6 +9,8 @@ const TargetBits = 32 const deferExtraRegs = 0 +const callInstSize = 4 // "bl someFunction" is 4 bytes + // Align on the maximum alignment for this platform (double). func align(ptr uintptr) uintptr { return (ptr + 7) &^ 7 diff --git a/src/runtime/arch_arm64.go b/src/runtime/arch_arm64.go index a481fb1a..ac28ab7c 100644 --- a/src/runtime/arch_arm64.go +++ b/src/runtime/arch_arm64.go @@ -7,6 +7,8 @@ const TargetBits = 64 const deferExtraRegs = 0 +const callInstSize = 4 // "bl someFunction" is 4 bytes + // Align on word boundary. func align(ptr uintptr) uintptr { return (ptr + 15) &^ 15 diff --git a/src/runtime/arch_avr.go b/src/runtime/arch_avr.go index 5fe06605..251d1543 100644 --- a/src/runtime/arch_avr.go +++ b/src/runtime/arch_avr.go @@ -11,6 +11,8 @@ const TargetBits = 8 const deferExtraRegs = 1 // the frame pointer (Y register) also needs to be stored +const callInstSize = 2 // "call" is 4 bytes, "rcall" is 2 bytes + // Align on a word boundary. func align(ptr uintptr) uintptr { // No alignment necessary on the AVR. diff --git a/src/runtime/arch_cortexm.go b/src/runtime/arch_cortexm.go index 45134405..6ea4a483 100644 --- a/src/runtime/arch_cortexm.go +++ b/src/runtime/arch_cortexm.go @@ -13,6 +13,8 @@ const TargetBits = 32 const deferExtraRegs = 0 +const callInstSize = 4 // "bl someFunction" is 4 bytes + // Align on word boundary. func align(ptr uintptr) uintptr { return (ptr + 7) &^ 7 diff --git a/src/runtime/arch_tinygoriscv.go b/src/runtime/arch_tinygoriscv.go index 904ff5d4..0d376c48 100644 --- a/src/runtime/arch_tinygoriscv.go +++ b/src/runtime/arch_tinygoriscv.go @@ -6,6 +6,8 @@ import "device/riscv" const deferExtraRegs = 0 +const callInstSize = 4 // 8 without relaxation, maybe 4 with relaxation + // RISC-V has a maximum alignment of 16 bytes (both for RV32 and for RV64). // Source: https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf func align(ptr uintptr) uintptr { diff --git a/src/runtime/arch_tinygowasm.go b/src/runtime/arch_tinygowasm.go index ac1a221c..c6e15b27 100644 --- a/src/runtime/arch_tinygowasm.go +++ b/src/runtime/arch_tinygowasm.go @@ -13,6 +13,8 @@ const TargetBits = 32 const deferExtraRegs = 0 +const callInstSize = 1 // unknown and irrelevant (llvm.returnaddress doesn't work), so make something up + //go:extern __heap_base var heapStartSymbol [0]byte diff --git a/src/runtime/arch_xtensa.go b/src/runtime/arch_xtensa.go index 03ca8c65..2ff4bf9e 100644 --- a/src/runtime/arch_xtensa.go +++ b/src/runtime/arch_xtensa.go @@ -9,6 +9,8 @@ const TargetBits = 32 const deferExtraRegs = 0 +const callInstSize = 3 // "callx0 someFunction" (and similar) is 3 bytes + // The largest alignment according to the Xtensa ABI is 8 (long long, double). func align(ptr uintptr) uintptr { return (ptr + 7) &^ 7 diff --git a/src/runtime/gc_blocks.go b/src/runtime/gc_blocks.go index 54c3cb91..7aaff89c 100644 --- a/src/runtime/gc_blocks.go +++ b/src/runtime/gc_blocks.go @@ -278,7 +278,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { } if interrupt.In() { - runtimePanic("alloc in interrupt") + runtimePanicAt(returnAddress(0), "alloc in interrupt") } gcTotalAlloc += uint64(size) @@ -318,7 +318,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { // Unfortunately the heap could not be increased. This // happens on baremetal systems for example (where all // available RAM has already been dedicated to the heap). - runtimePanic("out of memory") + runtimePanicAt(returnAddress(0), "out of memory") } } } diff --git a/src/runtime/panic.go b/src/runtime/panic.go index c9c69363..e8a67e4b 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -54,7 +54,19 @@ func _panic(message interface{}) { // Cause a runtime panic, which is (currently) always a string. func runtimePanic(msg string) { - printstring("panic: runtime error: ") + // 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 hasReturnAddr { + printstring("panic: runtime error at ") + printptr(uintptr(addr) - callInstSize) + printstring(": ") + } else { + printstring("panic: runtime error: ") + } println(msg) abort() } @@ -119,52 +131,52 @@ func _recover(useParentFrame bool) interface{} { // Panic when trying to dereference a nil pointer. func nilPanic() { - runtimePanic("nil pointer dereference") + runtimePanicAt(returnAddress(0), "nil pointer dereference") } // Panic when trying to add an entry to a nil map func nilMapPanic() { - runtimePanic("assignment to entry in nil map") + runtimePanicAt(returnAddress(0), "assignment to entry in nil map") } // Panic when trying to acces an array or slice out of bounds. func lookupPanic() { - runtimePanic("index out of range") + runtimePanicAt(returnAddress(0), "index out of range") } // Panic when trying to slice a slice out of bounds. func slicePanic() { - runtimePanic("slice out of range") + 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() { - runtimePanic("slice smaller than array") + 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() { - runtimePanic("unsafe.Slice/String: len out of range") + runtimePanicAt(returnAddress(0), "unsafe.Slice/String: len out of range") } // Panic when trying to create a new channel that is too big. func chanMakePanic() { - runtimePanic("new channel is too big") + runtimePanicAt(returnAddress(0), "new channel is too big") } // Panic when a shift value is negative. func negativeShiftPanic() { - runtimePanic("negative shift") + runtimePanicAt(returnAddress(0), "negative shift") } // Panic when there is a divide by zero. func divideByZeroPanic() { - runtimePanic("divide by zero") + runtimePanicAt(returnAddress(0), "divide by zero") } func blockingPanic() { - runtimePanic("trying to do blocking operation in exported function") + runtimePanicAt(returnAddress(0), "trying to do blocking operation in exported function") }