Skip to content

Commit

Permalink
unix: print a message when a fatal signal happens
Browse files Browse the repository at this point in the history
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 #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.
  • Loading branch information
aykevl authored and deadprogram committed Aug 15, 2024
1 parent 815784b commit 194396d
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 7 deletions.
4 changes: 4 additions & 0 deletions compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/macos-minimal-sdk
7 changes: 6 additions & 1 deletion src/runtime/arch_386.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/arch_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/arch_arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/arch_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/arch_mips.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/arch_mipsle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/os_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/os_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 56 additions & 0 deletions src/runtime/runtime_unix.c
Original file line number Diff line number Diff line change
@@ -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 <signal.h>
#include <unistd.h>
#include <stdint.h>
#include <ucontext.h>
#include <string.h>

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);
}
51 changes: 51 additions & 0 deletions src/runtime/runtime_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 194396d

Please sign in to comment.