From 194396d71570318eb581245c49778341474d8ef6 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 12 Aug 2024 18:45:22 +0200 Subject: [PATCH] unix: print a message when a fatal signal happens Print a message for SIGBUS, SIGSEGV, and SIGILL when they happen. These signals are always fatal, but it's very useful to know which of them happened. Also, it prints the location in the binary which can then be parsed by `tinygo run` (see https://github.com/tinygo-org/tinygo/pull/4383). While this does add some extra binary size, it's for Linux and MacOS (systems that typically have plenty of RAM/storage) and could be very useful when debugging some low-level crash such as a runtime bug. --- compileopts/target.go | 4 +++ lib/macos-minimal-sdk | 2 +- src/runtime/arch_386.go | 7 ++++- src/runtime/arch_amd64.go | 7 ++++- src/runtime/arch_arm.go | 7 ++++- src/runtime/arch_arm64.go | 7 ++++- src/runtime/arch_mips.go | 7 ++++- src/runtime/arch_mipsle.go | 7 ++++- src/runtime/os_darwin.go | 8 ++++++ src/runtime/os_linux.go | 6 ++++ src/runtime/runtime_unix.c | 56 +++++++++++++++++++++++++++++++++++++ src/runtime/runtime_unix.go | 51 +++++++++++++++++++++++++++++++++ 12 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/runtime/runtime_unix.c diff --git a/compileopts/target.go b/compileopts/target.go index 3368e20c4e..501d99f119 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -388,6 +388,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { "-arch", llvmarch, "-platform_version", "macos", platformVersion, platformVersion, ) + spec.ExtraFiles = append(spec.ExtraFiles, + "src/runtime/runtime_unix.c") case "linux": spec.Linker = "ld.lld" spec.RTLib = "compiler-rt" @@ -407,6 +409,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { // proper threading. spec.CFlags = append(spec.CFlags, "-mno-outline-atomics") } + spec.ExtraFiles = append(spec.ExtraFiles, + "src/runtime/runtime_unix.c") case "windows": spec.Linker = "ld.lld" spec.Libc = "mingw-w64" diff --git a/lib/macos-minimal-sdk b/lib/macos-minimal-sdk index ebb736fda2..91ac2eabd8 160000 --- a/lib/macos-minimal-sdk +++ b/lib/macos-minimal-sdk @@ -1 +1 @@ -Subproject commit ebb736fda2bec7cea38dcda807518b835a539525 +Subproject commit 91ac2eabd80f10d95cb4255c78999d9d2c45a3be diff --git a/src/runtime/arch_386.go b/src/runtime/arch_386.go index 4e9cce72ba..90ec8e8baf 100644 --- a/src/runtime/arch_386.go +++ b/src/runtime/arch_386.go @@ -9,7 +9,12 @@ const deferExtraRegs = 0 const callInstSize = 5 // "call someFunction" is 5 bytes -const linux_MAP_ANONYMOUS = 0x20 +const ( + linux_MAP_ANONYMOUS = 0x20 + linux_SIGBUS = 7 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // Align on word boundary. func align(ptr uintptr) uintptr { diff --git a/src/runtime/arch_amd64.go b/src/runtime/arch_amd64.go index 3bb03e3c71..436d6e3849 100644 --- a/src/runtime/arch_amd64.go +++ b/src/runtime/arch_amd64.go @@ -9,7 +9,12 @@ const deferExtraRegs = 0 const callInstSize = 5 // "call someFunction" is 5 bytes -const linux_MAP_ANONYMOUS = 0x20 +const ( + linux_MAP_ANONYMOUS = 0x20 + linux_SIGBUS = 7 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // Align a pointer. // Note that some amd64 instructions (like movaps) expect 16-byte aligned diff --git a/src/runtime/arch_arm.go b/src/runtime/arch_arm.go index e28e854102..ea6b540d2a 100644 --- a/src/runtime/arch_arm.go +++ b/src/runtime/arch_arm.go @@ -11,7 +11,12 @@ const deferExtraRegs = 0 const callInstSize = 4 // "bl someFunction" is 4 bytes -const linux_MAP_ANONYMOUS = 0x20 +const ( + linux_MAP_ANONYMOUS = 0x20 + linux_SIGBUS = 7 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // Align on the maximum alignment for this platform (double). func align(ptr uintptr) uintptr { diff --git a/src/runtime/arch_arm64.go b/src/runtime/arch_arm64.go index 4e798e36b1..6d3c856cf6 100644 --- a/src/runtime/arch_arm64.go +++ b/src/runtime/arch_arm64.go @@ -9,7 +9,12 @@ const deferExtraRegs = 0 const callInstSize = 4 // "bl someFunction" is 4 bytes -const linux_MAP_ANONYMOUS = 0x20 +const ( + linux_MAP_ANONYMOUS = 0x20 + linux_SIGBUS = 7 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // Align on word boundary. func align(ptr uintptr) uintptr { diff --git a/src/runtime/arch_mips.go b/src/runtime/arch_mips.go index bfaf890ae5..5a7d05c898 100644 --- a/src/runtime/arch_mips.go +++ b/src/runtime/arch_mips.go @@ -9,7 +9,12 @@ const deferExtraRegs = 0 const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot -const linux_MAP_ANONYMOUS = 0x800 +const ( + linux_MAP_ANONYMOUS = 0x800 + linux_SIGBUS = 10 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // It appears that MIPS has a maximum alignment of 8 bytes. func align(ptr uintptr) uintptr { diff --git a/src/runtime/arch_mipsle.go b/src/runtime/arch_mipsle.go index b6bf7d5169..498cf862b7 100644 --- a/src/runtime/arch_mipsle.go +++ b/src/runtime/arch_mipsle.go @@ -9,7 +9,12 @@ const deferExtraRegs = 0 const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot -const linux_MAP_ANONYMOUS = 0x800 +const ( + linux_MAP_ANONYMOUS = 0x800 + linux_SIGBUS = 10 + linux_SIGILL = 4 + linux_SIGSEGV = 11 +) // It appears that MIPS has a maximum alignment of 8 bytes. func align(ptr uintptr) uintptr { diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index eeb192dda8..9255fb90f2 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -22,6 +22,14 @@ const ( clock_MONOTONIC_RAW = 4 ) +// Source: +// https://opensource.apple.com/source/xnu/xnu-7195.141.2/bsd/sys/signal.h.auto.html +const ( + sig_SIGBUS = 10 + sig_SIGILL = 4 + sig_SIGSEGV = 11 +) + // https://opensource.apple.com/source/xnu/xnu-7195.141.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html type machHeader struct { magic uint32 diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 403f00246a..df5870a2df 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -23,6 +23,12 @@ const ( clock_MONOTONIC_RAW = 4 ) +const ( + sig_SIGBUS = linux_SIGBUS + sig_SIGILL = linux_SIGILL + sig_SIGSEGV = linux_SIGSEGV +) + // For the definition of the various header structs, see: // https://refspecs.linuxfoundation.org/elf/elf.pdf // Also useful: diff --git a/src/runtime/runtime_unix.c b/src/runtime/runtime_unix.c new file mode 100644 index 0000000000..79dd7ce915 --- /dev/null +++ b/src/runtime/runtime_unix.c @@ -0,0 +1,56 @@ +//go:build none + +// This file is included on Darwin and Linux (despite the //go:build line above). + +#define _GNU_SOURCE +#define _XOPEN_SOURCE +#include +#include +#include +#include +#include + +void tinygo_handle_fatal_signal(int sig, uintptr_t addr); + +static void signal_handler(int sig, siginfo_t *info, void *context) { + ucontext_t* uctx = context; + uintptr_t addr = 0; + #if __APPLE__ + #if __arm64__ + addr = uctx->uc_mcontext->__ss.__pc; + #elif __x86_64__ + addr = uctx->uc_mcontext->__ss.__rip; + #else + #error unknown architecture + #endif + #elif __linux__ + // Note: this can probably be simplified using the MC_PC macro in musl, + // but this works for now. + #if __arm__ + addr = uctx->uc_mcontext.arm_pc; + #elif __i386__ + addr = uctx->uc_mcontext.gregs[REG_EIP]; + #elif __x86_64__ + addr = uctx->uc_mcontext.gregs[REG_RIP]; + #else // aarch64, mips, maybe others + addr = uctx->uc_mcontext.pc; + #endif + #else + #error unknown platform + #endif + tinygo_handle_fatal_signal(sig, addr); +} + +void tinygo_register_fatal_signals(void) { + struct sigaction act = { 0 }; + // SA_SIGINFO: we want the 2 extra parameters + // SA_RESETHAND: only catch the signal once (the handler will re-raise the signal) + act.sa_flags = SA_SIGINFO | SA_RESETHAND; + act.sa_sigaction = &signal_handler; + + // Register the signal handler for common issues. There are more signals, + // which can be added if needed. + sigaction(SIGBUS, &act, NULL); + sigaction(SIGILL, &act, NULL); + sigaction(SIGSEGV, &act, NULL); +} diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 8c5a42ff7c..ba5d5a5938 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -26,6 +26,9 @@ func abort() //export exit func exit(code int) +//export raise +func raise(sig int32) + //export clock_gettime func libc_clock_gettime(clk_id int32, ts *timespec) @@ -74,6 +77,10 @@ func main(argc int32, argv *unsafe.Pointer) int { main_argc = argc main_argv = argv + // Register some fatal signals, so that we can print slightly better error + // messages. + tinygo_register_fatal_signals() + // Obtain the initial stack pointer right before calling the run() function. // The run function has been moved to a separate (non-inlined) function so // that the correct stack pointer is read. @@ -119,6 +126,50 @@ func runMain() { run() } +//export tinygo_register_fatal_signals +func tinygo_register_fatal_signals() + +// Print fatal errors when they happen, including the instruction location. +// With the particular formatting below, `tinygo run` can extract the location +// where the signal happened and try to show the source location based on DWARF +// information. +// +//export tinygo_handle_fatal_signal +func tinygo_handle_fatal_signal(sig int32, addr uintptr) { + if panicStrategy() == panicStrategyTrap { + trap() + } + + // Print signal including the faulting instruction. + if addr != 0 { + printstring("panic: runtime error at ") + printptr(addr) + } else { + printstring("panic: runtime error") + } + printstring(": caught signal ") + switch sig { + case sig_SIGBUS: + println("SIGBUS") + case sig_SIGILL: + println("SIGILL") + case sig_SIGSEGV: + println("SIGSEGV") + default: + println(sig) + } + + // TODO: it might be interesting to also print the invalid address for + // SIGSEGV and SIGBUS. + + // Do *not* abort here, instead raise the same signal again. The signal is + // registered with SA_RESETHAND which means it executes only once. So when + // we raise the signal again below, the signal isn't handled specially but + // is handled in the default way (probably exiting the process, maybe with a + // core dump). + raise(sig) +} + //go:extern environ var environ *unsafe.Pointer