After creating a wrapper in Swift to implement objective c try…catch (see other blog piece), I started to get interested in how exceptions work. Also I’ve been doing a lot of looking at unix and c to get those basics right at a deep level. My day job is an iPhone developer. iOS and OSX are unix and the programs you use/write on them are a mix of Swift, Objective C, C++ and C. Like all plain C programs or C++ programs, they run “on top of” an operating system, bsd/darwin unix variants in our case but the principles are similar in linux.
NSEXCEPTION
My unit tests for this Swift exception handling framework deliberately raise and throw an NSException. This hits the standard exception breakpoint if run in the debugger. A slight nuisance when running the test framework in Xcode (it wouldn’t be an issue on Jenkins or other CI servers). That got me thinking and I started to poke inside.
The standard breakpoint is actually two breakpoints.
(lldb) breakpoint list
Current breakpoints:
1: names = {‘-[SenTestCase failWithException:]’, ‘_XCTFailureHandler’}, locations = 1, resolved = 1, hit count = 0
1.1: where = XCTest`_XCTFailureHandler, address = 0x000000010460e01c, resolved, hit count = 0
2: names = {‘objc_exception_throw’, ‘__cxa_throw’}, locations = 2, resolved = 2, hit count = 1
2.1: where = libobjc.A.dylib`objc_exception_throw, address = 0x0000000104b6ddbb, resolved, hit count = 1
2.2: where = libc++abi.dylib`__cxa_throw, address = 0x00000001078a7c6b, resolved, hit count = 0
Ignore the first breakpoint. The second one is a group of two locations, objc_exception_throw and __cxa_throw. These reflect the objective c exceptions breakpoint and the C++ exceptions breakpoint. Might seem a bit odd but some people write in c++ and many of the libraries you call are written in c++ (CoreAnimation for example is C++ I believe).
This excellent blog piece by Nicolas Brailovsky explains how C++ exceptions work: https://monoinfinito.wordpress.com/2013/02/05/c-exceptions-under-the-hood/
What about objective c? How does it work? Are signals involved? In most c code, it seems that assert() calls raise() which calls kill() to send a signal SIGABRT to the current process.
In this case it’s a bit different. Here’s the disassembly for the objc_exception_throw that is frozen mid-run when the exception is raised:
libobjc.A.dylib`objc_exception_throw:
-> 0x104b6ddbb <+0>: pushq %rbp
0x104b6ddbc <+1>: movq %rsp, %rbp
0x104b6ddbf <+4>: pushq %r15
0x104b6ddc1 <+6>: pushq %r14
0x104b6ddc3 <+8>: pushq %r13
0x104b6ddc5 <+10>: pushq %r12
0x104b6ddc7 <+12>: pushq %rbx
0x104b6ddc8 <+13>: subq $0xfa8, %rsp ; imm = 0xFA8
0x104b6ddcf <+20>: movq %rdi, %rbx
0x104b6ddd2 <+23>: movl $0x20, %edi
0x104b6ddd7 <+28>: callq 0x104b8451c ; symbol stub for: __cxa_allocate_exception
0x104b6dddc <+33>: movq %rax, %r15
0x104b6dddf <+36>: movq %r15, %r14
0x104b6dde2 <+39>: movq %rbx, %rdi
0x104b6dde5 <+42>: callq *0x35e48d(%rip) ; exception_preprocessor
0x104b6ddeb <+48>: movq %rax, %rbx
0x104b6ddee <+51>: movq 0x35e20b(%rip), %rsi ; “retain”
0x104b6ddf5 <+58>: movq %rbx, %rdi
0x104b6ddf8 <+61>: callq *0x41d2aa(%rip) ; (void *)0x0000000104b83800: objc_msgSend
0x104b6ddfe <+67>: movq %rbx, (%r15)
0x104b6de01 <+70>: leaq 0x35e418(%rip), %rax ; objc_ehtype_vtable + 16
0x104b6de08 <+77>: movq %rax, 0x8(%r15)
0x104b6de0c <+81>: movq %rbx, %rdi
0x104b6de0f <+84>: callq 0x104b6c36f ; object_getClassName
0x104b6de14 <+89>: movq %rax, 0x10(%r15)
0x104b6de18 <+93>: xorl %eax, %eax
0x104b6de1a <+95>: testq %rbx, %rbx
0x104b6de1d <+98>: je 0x104b6de36 ; <+123>
0x104b6de1f <+100>: movq %rbx, %rax
0x104b6de22 <+103>: jns 0x104b6de33 ; <+120>
0x104b6de24 <+105>: shrq $0x39, %rax
0x104b6de28 <+109>: andq $0x78, %rax
0x104b6de2c <+113>: addq 0x41d26d(%rip), %rax ; (void *)0x0000000104ecc650: objc_debug_taggedpointer_classes
0x104b6de33 <+120>: movq (%rax), %rax
0x104b6de36 <+123>: movq %rax, 0x18(%r14)
0x104b6de3a <+127>: leaq 0x416bf6(%rip), %r12 ; PrintExceptions
0x104b6de41 <+134>: cmpb $0x0, (%r12)
0x104b6de46 <+139>: je 0x104b6de67 ; <+172>
0x104b6de48 <+141>: movq %rbx, %rdi
0x104b6de4b <+144>: callq 0x104b6c36f ; object_getClassName
0x104b6de50 <+149>: movq %rax, %rcx
0x104b6de53 <+152>: leaq 0x175d3(%rip), %rdi ; “EXCEPTIONS: throwing %p (object %p, a %s)”
0x104b6de5a <+159>: xorl %eax, %eax
0x104b6de5c <+161>: movq %r14, %rsi
0x104b6de5f <+164>: movq %rbx, %rdx
0x104b6de62 <+167>: callq 0x104b6d89b ; _objc_inform
0x104b6de67 <+172>: addq $0x8, %r15
0x104b6de6b <+176>: leaq 0x416bc6(%rip), %rax ; PrintExceptionThrow
0x104b6de72 <+183>: cmpb $0x0, (%rax)
0x104b6de75 <+186>: je 0x104b6ded0 ; <+277>
0x104b6de77 <+188>: cmpb $0x0, (%r12)
0x104b6de7c <+193>: jne 0x104b6de9d ; <+226>
0x104b6de7e <+195>: movq %rbx, %rdi
0x104b6de81 <+198>: callq 0x104b6c36f ; object_getClassName
0x104b6de86 <+203>: movq %rax, %rcx
0x104b6de89 <+206>: leaq 0x1759d(%rip), %rdi ; “EXCEPTIONS: throwing %p (object %p, a %s)”
0x104b6de90 <+213>: xorl %eax, %eax
0x104b6de92 <+215>: movq %r14, %rsi
0x104b6de95 <+218>: movq %rbx, %rdx
0x104b6de98 <+221>: callq 0x104b6d89b ; _objc_inform
0x104b6de9d <+226>: leaq -0xfd0(%rbp), %r12
0x104b6dea4 <+233>: movl $0x1f4, %esi ; imm = 0x1F4
0x104b6dea9 <+238>: movq %r12, %rdi
0x104b6deac <+241>: callq 0x104b845e2 ; symbol stub for: backtrace
0x104b6deb1 <+246>: movl %eax, %r13d
0x104b6deb4 <+249>: movq 0x41d1ad(%rip), %rax ; (void *)0x0000000107aaac10: __stderrp
0x104b6debb <+256>: movq (%rax), %rdi
0x104b6debe <+259>: callq 0x104b84624 ; symbol stub for: fileno
0x104b6dec3 <+264>: movq %r12, %rdi
0x104b6dec6 <+267>: movl %r13d, %esi
0x104b6dec9 <+270>: movl %eax, %edx
0x104b6decb <+272>: callq 0x104b845ee ; symbol stub for: backtrace_symbols_fd
0x104b6ded0 <+277>: movq %rbx, %rdi
0x104b6ded3 <+280>: nop
0x104b6ded4 <+281>: nopl (%rax)
0x104b6ded8 <+285>: leaq 0xb(%rip), %rdx ; _objc_exception_destructor(void*)
0x104b6dedf <+292>: movq %r14, %rdi
0x104b6dee2 <+295>: movq %r15, %rsi
0x104b6dee5 <+298>: callq 0x104b8453a ; symbol stub for: __cxa_throw
I’m not an assembler expert but it looks like this is doing the sort of things you might expect __cxa_allocate_exception and __cxa_throw internally. As Nicolas’ blog explains, this is how the C++ raise function works, which is a bit unexpected.
Deepening the mystery, when I googled for source code to objc_exception_throw, I got the objc runtime source code https://opensource.apple.com/source/objc4/objc4-437/runtime/objc-exception.m and it seems to have a simple definition that does some things with linked lists (probably try…catch…finally blocks) then calls longjmp to do an old-fashioned c type unwind… demolishing the stack in between.
But further down the same file there is another definition of objc_exception_throw which seems to be the one called, and that wraps the C++ system. Looking in the release notes, this is the one called in the 64 bit ABI, and is the one I’m seeing in my debugger.
Correction: actually the first version is the objective c runtime 1, which existed on OSX up to 10.4 (Tiger) and has been replaced by objective c runtime 2 since OSX 10.5 (Leopard, Snow Leopard, Lion, etc.) and all versions of iOS.
The implementation of __cxa_throw in the standard c++ library causes the app to exit on unhanded exceptions. Objective c and cocoa touch always had an “unhanded exception handler” mechanism (this is used by crash reporting tools such as Crashlytics), which must presumably be different.
If the exception is unhandled and goes into std::terminate then that will probably either call the OS exit() call or abort(). These are handled differently, the first is an operating system call to exit the process. The second raises a signal that is usually unhandled so causes program termination.
ASSERT/SIGNAL/SIGSEGV
You can see these are different in the debugger as the line is highlighted red when the debugger traps these signals.
As a result, try…catch will not usually catch assertion failures. But it depends.
In Objective C, it’s standard (ish) practice to use NSAssert, which raises an NSException so is handled by try…catch (also it’s disabled by NS_BLOCK_ASSERTIONS).
assert is much more primitive and will pretty much always cause a crash