I am building some rudimentary crash logging system based on this blog post (Yes, I am aware of PLCRashReporter, and No, I can't use it and need to roll my own, however limited. Thank you).
My code registers both an exception handler (with NSSetUncaughtExceptionHandler()
) and a signal handler for most signals:
SIGQUIT
SIGILL
SIGTRAP
SIGABRT
SIGEMT
SIGFPE
SIGBUS
SIGSEGV
SIGSYS
SIGPIPE
SIGALRM
SIGXCPU
SIGXFSZ
It seems to work fine with basic Objective-C stuff such as "unrecognized selector sent to instance...", etc.
Next, I tried it with the following Swift code:
var empty:String! = nil
let index = empty.startIndex // < KA-BOOM!
...but the thrown exception EXC_BAD_INSTRUCTION
is not caught (my handler is not called), and instead I get the familiar:
fatal error: unexpectedly found nil while unwrapping an Optional value
...in the Xcode console.
What am I missing?
This answer says that this kind of runtime error can be handled with SIGTRAP
; however I am already handling that signal and it doesn't seem to work.
ADDENDUM: It seems there is no centralized exception handling system in place for Swift runtime errors (out of bounds, nil unwrap, etc.); see this post. However, Xcode is still able to crash with EXC_BAD_INSTRUCTION
, so my question stands: How is this possible, and why can't my signal handlers handle this?
ADDENDUM 2: Just to be safe, I tried registering my signal handler for all 20+ signals defined in signal.h (not just the ones listed above); still, no change.
ADDENDUM 3: Apple's official document Understanding and Analyzing Application Crash Reports contains this snippet:
Trace Trap [EXC_BREAKPOINT // SIGTRAP]
Similar to an Abnormal Exit, this exception is intended to give an attached debugger the chance to interrupt the process at a specific point in its execution. You can trigger this exception from your own code using the __builtin_trap() function. If no debugger is attached, the process is terminated and a crash report is generated.
Lower-level libraries (e.g, libdispatch) will trap the process upon encountering a fatal error. Additional information about the error can be found in the Additional Diagnostic Information section of the crash report, or in the device's console.
Swift code will terminate with this exception type if an unexpected condition is encountered at runtime such as:
- a non-optional type with a nil value
- a failed forced type conversion
Look at the Backtraces to determine where the unexpected condition was encountered. Additional information may have also been logged to the device's console. You should modify the code at the crashing location to gracefully handle the runtime failure. For example, use Optional Binding instead of force unwrapping an optional.
(emphasis mine)
Still: Why Can't I Handle This With My SIGTRAP
Signal Handler?
TL;DR: Signal handler works fine when debugger is not attached.
In order to see what was happening, I set up a small demo project where I registered a signal handler for
SIGUSR1
(one of the 'user-defined' signals available) and subsequently sent said signal to my process using thekill()
function:At first, I wasn't able to have Xcode's debugger jump to my signal handler, and instead it would stop at the line with the
kill()
call, highlighting it in red just as in my question.After reading this question and its answers, it struck me that the debugger must pretty much depend on signals in order to be able to stop/resume program execution e.g., at breakpoints.
Using the symbolic breakpoint specified in one of the answers:
...I was able to make Xcode not stop on this command:
...and instead jump to my signal handler, all while debugging. I could even step inside the signal handler and execute its statements one by one; display an alert view from within it, etc.
Although I am not able to achieve the same result with
SIGTRAP
(or evenSIGABRT
), my signal handler does get called (and my crash log does get saved to disk!) when I launch my app without a debugger attached (e.g., by tapping the app's icon on the home screen) and have it throw a Swift runtime error like the one I mentioned in my question.My guess on why I was able to intercept
unrecognized selector sent to instance...
seems to be that, as an Objective-C exception, it is an entirely different beast from Unix signals, and has nothing to do with the workings of the debugger (unless you set up an exception breakpoint for Objective-C exceptions?).The very fact that the handler is set with the unrelated function
NSSetUncaughtExceptionHandler()
should have been a hint...(I'm pretty much talking out of my limited knowledge here, so please feel free to add answers/comments with corrections as you see fit)