Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to intercept EXC_BAD_INSTRUCTION when unwrapping Nil

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?

like image 268
Nicolas Miari Avatar asked Nov 16 '16 11:11

Nicolas Miari


1 Answers

TL;DR: Signal handler works fine when debugger is not attached.

(We can even catch fatal_error's SIGTRAP signal, and log stack-trace)


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 the kill() function:

kill(0, SIGUSR1);

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:

process handle SIGUSR1 -n true -p true -s false

...I was able to make Xcode not stop on this command:

kill(pid, SIGUSR1);

...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 even SIGABRT), 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)

like image 54
Nicolas Miari Avatar answered Sep 29 '22 10:09

Nicolas Miari