Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Example of how Objective-C's @try-@catch implementation is executed at runtime?

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!

like image 724
TooTallNate Avatar asked Aug 15 '11 07:08

TooTallNate


1 Answers

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.

like image 183
CodaFi Avatar answered Sep 20 '22 14:09

CodaFi