In Objective-C's low-level runtime headers (/usr/include/objc
), there is an objc-exceptions.h
file. It would seem this is how @try
/@catch
is implemented by the ObjC compiler.
I am trying to invoke these functions manually (for experimentations with the ObjC runtime and implementation) in order to catch an "unrecognized selector sent to class" exception.
So basically, all I'm looking for is an example of how to do a @try
/@catch
using the low-level runtime functions. Thanks in advance!
So you want to know how the runtime does exception handling?
Prepare to be disappointed.
Because it doesn't. ObjC doesn't have an exception-handling ABI, only SPI which you've already found. No doubt you've also discovered that the Objective-C exception ABI is actually the exact same one as the C++ exception handling ABI. To that end, let's get started with some code.
#include <Foundation/Foundation.h>
int main(int argc, char **argv) {
@try {
@throw [NSException exceptionWithName:@"ExceptionalCircumstances" reason:@"Drunk on power" userInfo:nil];
} @catch(...) {
NSLog(@"Catch");
} @finally {
NSLog(@"Finally");
}
}
Run through clang with -ObjC -O3
(and stripped of a disgusting amount of debug information) we get this:
_main: ## @main
push rbp
mov rbp, rsp
push r14
push rbx
mov rdi, qword ptr [rip + L_OBJC_CLASSLIST_REFERENCES_$_]
mov rsi, qword ptr [rip + L_OBJC_SELECTOR_REFERENCES_]
lea rdx, qword ptr [rip + L__unnamed_cfstring_]
lea rcx, qword ptr [rip + L__unnamed_cfstring_2]
xor r8d, r8d
call qword ptr [rip + _objc_msgSend@GOTPCREL]
mov rdi, rax
call _objc_exception_throw
LBB0_2:
mov rdi, rax
call _objc_begin_catch
lea rdi, qword ptr [rip + L__unnamed_cfstring_4]
xor eax, eax
call _NSLog
call _objc_end_catch
xor ebx, ebx
LBB0_8:
lea rdi, qword ptr [rip + L__unnamed_cfstring_6]
xor eax, eax
call _NSLog
test bl, bl
jne LBB0_10
LBB0_11:
xor eax, eax
pop rbx
pop r14
pop rbp
ret
LBB0_5:
mov rbx, rax
call _objc_end_catch
jmp LBB0_7
LBB0_6:
mov rbx, rax
LBB0_7:
mov rdi, rbx
call _objc_begin_catch
mov bl, 1
jmp LBB0_8
LBB0_12:
mov r14, rax
test bl, bl
je LBB0_14
jmp LBB0_13
LBB0_10:
call _objc_exception_rethrow
jmp LBB0_11
LBB0_16: ## %.thread
mov r14, rax
LBB0_13:
call _objc_end_catch
LBB0_14:
mov rdi, r14
call __Unwind_Resume
LBB0_15:
call _objc_terminate
If you compile it with ObjC++ nothing changes. (Well, that's not entirely true. The last _objc_terminate
turns into a jump into clang's personal ___clang_call_terminate
routine). Anyhow, this code can be divided into 3 important sections. The first is from _main
to the start of LBB0_2
, or where our try block happens. Because we're blatantly throwing an exception and catching it in our try
block, the compiler has gone ahead and removed the branch around LBB0_2
and moved straight to the catch handlers. At this point Objective-C, or more accurately CoreFoundation, has set up an exception object for us and libC++ has begun searching for an exception handler during the requisite unwinding phase.
The second important block of code is from LBB0_2
to the end of LBB0_11
where our catch
and finally
blocks live. Because all is well, all the code below this is dead (and hopefully gets stripped in release), but let's imagine it wasn't.
The third part is from LBB0_8
on down where the compiler would have emitted a jump to from the NSLog in LBB0_2
if we'd done something stupid like, say, tried not to catch our exception. This handler instead flips a bit after calling into the objc_begin_catch
that causes us to branch around the ret
and move onto the objc_exception_rethrow()
that tells the unwind handler that we've dropped the ball and to continue searching for handlers somewhere else. Of course, we're main, so there are no other handlers, and std::terminate
gets invoked as we leave.
All this to say you're gonna have a bad time if you want to try to write this stuff out by hand. All the __cxa_*
and ObjC SPI functions throw around exception objects in ways you can't rely on and (rather pessimistically many) handlers are emitted in a very tight order to make sure the C++ ABI contract is fulfilled because if it isn't the spec mandates std::terminate
be called. If you'd like to take an active listening role, you are allowed to redefine the exception handling stuff with your own functions and Objective-C has objc_setUncaughtExceptionHandler
, objc_setExceptionMatcher
objc_setExceptionPreprocessor
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With