Programming

More poking in exceptions


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

 

Leave a comment