This is the fifth day of my participation in the August More text Challenge. For details, see:August is more challenging

Hi 👋

  • Wechat: RyukieW
  • 📦 Technical article archive
  • 🐙 making
My personal project Mine Elic endless sky ladder Dream of books
type The game financial
AppStore Elic Umemi

preface

In the previous article, we explored some of the underlying principles of objc_msgSend in the debugging interpretation of objc_msgSend source code.

  • We found this in the slow search process
    • Forward_imp Message forwarding
    • ResolveMethod_locked Dynamic resolution
  • This article will explore these two places in depth
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    constIMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass; .for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {... }else{...if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break; }}... }// No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior); }... done_unlock: runtimeLock.unlock(a);if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

Copy the code

I attach a flowchart first

  • It would be difficult to analyze only from the source code, but combining the code to debug various cases, and this flowchart can better understand the details of the entire process
  • I did a lot of debugging myself to get the same idea. I knew these things before, but didn’t go deep enough to understand the details
  • This picture has also been altered several times

A, _objc_msgForward_impcache

We found the core code step by step by searching the source code, objc-msg-arm64.s

__objc_msgForward_impcache

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
Copy the code

__objc_msgForward

ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward
Copy the code

__objc_forward_handler

  • We didn’t find it__objc_forward_handler
    • Suspect not assembly code, removed_Have a try

objc_defaultForwardHandler

The following code is found.

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    /** Print out some information. Here's the new crash related to unrecognized selectors that we often see */ 
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)".class_isMetaClass(object_getClass(self)) ? '+' : The '-'.object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
Copy the code

Second, the resolveMethod_locked

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{...if (! cls->isMetaClass()) {
        // Object methods (CLS is not a metaclass), understanding the ISA diagram will be clearer here
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        / / class methods
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls); }}// chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
Copy the code

Three, instance method dynamic resolution

Here we declare an instance method, but do not implement it. Trigger calls

- (void)noIMP;

[obj noIMP];
Copy the code

You’ll see a common crash message:

libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RYModel noIMP]: unrecognized selector sent to instance 0x100677a40'
terminating with uncaught exception of type NSException
Copy the code

3.1 Dynamically Adding Methods

Rewrite + (BOOL)resolveInstanceMethod:(SEL) SEL add method

- (void)callNoIMPSel {
    NSLog(@"Called SEL without IMP");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(noIMP)) {
        IMP safeIMP = class_getMethodImplementation(self, @selector(callNoIMPSel));
        Method method =  class_getInstanceMethod(self, @selector(callNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"Dynamic resolution of %@".NSStringFromSelector(sel));
        return class_addMethod(self, sel, safeIMP, type);
    }
    return [super resolveInstanceMethod:sel];
}
Copy the code

Output:

Dynamic resolution on noIMP calls SEL without IMPCopy the code

3.2 If it is called again, will dynamic resolution be carried out?

Theoretical analysis: When a Method is added the first time, it is inserted into the Method cache synchronously, so it is not added the second time.

The results of the actual operation also confirm the analysis.

Dynamic resolution of noIMP calls SEL without IMP calls SEL without IMPCopy the code

3.3 Source code Parsing

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{... SEL resolve_sel = @selector(resolveInstanceMethod:); .BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // Call objc_msgSend directly to resolveInstanceMethod:
    bool resolved = msg(cls, resolve_sel, sel);

    // Dynamic resolution processing comes here later
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

        if (resolved  &&  PrintResolving) {
            if (imp) {
                ...
            }
            else {
                // Method resolver didn't add anything?. }}}Copy the code
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    ...

    IMP imp = cache_getImp(cls, sel);
    if(imp ! =NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES.#endif
    if (slowpath(imp == NULL)) {
        / / to find again
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        // Forwarding is required
        return nil;
    }
    return imp;
}
Copy the code

Four, class method dynamic resolution

4.1 Dynamically Adding Methods

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(noClassIMP)) {
        IMP safeIMP = class_getMethodImplementation(self, @selector(callClassNoIMPSel));
        Method method =  class_getClassMethod(self, @selector(callClassNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"Dynamic resolution on %@ (class method)".NSStringFromSelector(sel));
        return class_addMethod(self, sel, safeIMP, type);
    }
    return [super resolveClassMethod:sel];
}
Copy the code
  • If you write it like this, you’ll find that it still crashes. It didn’t work.
  • Why is that?
  • If you don’t know why, you need to figure it out first, okayWhere do class methods exist
    • This article explores the structure of classes in depth

Correct implementation:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(noClassIMP)) {
        Class meta = objc_getMetaClass("RYModel");
        IMP safeIMP = class_getMethodImplementation(meta, @selector(callClassNoIMPSel));
        Method method =  class_getClassMethod(meta, @selector(callClassNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"Dynamic resolution on %@ (class method)".NSStringFromSelector(sel));
        return class_addMethod(meta, sel, safeIMP, type);
    }
    return [super resolveClassMethod:sel];
}
Copy the code

Output:

Dynamic resolution (Class method) on noClassIMP calls SEL of Class without IMPCopy the code

4.2 Special handling of dynamic resolution of class methods

If there is no corresponding Method in the cache after the resolveClassMethod, resolveInstanceMethod will be called again.

// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}
Copy the code

So the Apple engineers were very upset and gave us a lot of chances not to crash

5. Message forwarding

In lookUpImpOrForward we find _objc_msgForward_impcache

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    constIMP forward_imp = (IMP)_objc_msgForward_impcache; . }Copy the code

5.1 Looking for Pointcuts

It’s actually in objc-msg-arm64.s

__objc_msgForward_impcache

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
Copy the code

__objc_msgForward

ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward
Copy the code

_objc_forward_handler

  • We got a little closer_objc_forward_handler
    • Has a default implementation:objc_defaultForwardHandler
    • And asetMethods:objc_setForwardHandler
#if! __OBJC2__.#else
    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
        _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                    "(no message forward handler is installed)".class_isMetaClass(object_getClass(self)) ? '+' : The '-'.object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    #if SUPPORT_STRET (ARM64 = 0).#endif

#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

Copy the code

A further search for setForwardHandler did not find the call, presumably not in the OC source code.

Analysis of the Log

Found that the ___forwarding___ method in CoreFoundation was called.

*** First throw call stack:
(
    ...
	3   CoreFoundation                      0x00007fff203f790b ___forwarding___ + 1448
	4   CoreFoundation                      0x00007fff203f72d8 _CF_forwarding_prep_0 + 120...).Copy the code

So how do you go further?

5.2 Compile and analyze CoreFoundation ‘forwarding:

forwardingTargetForSelector:

0x7fff203f741b <+184>: mov r14, qword ptr [rip + 0x604ed906] ; "forwardingTargetForSelector:"

By the instruction we find a forwardingTargetForSelector:

Let’s look at the source code and assume from the name that this method is used to forward a message to an object.

Our next break point:

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
Copy the code

If the breakpoint does reach this point, output the stack information:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001003548f2 libobjc.A.dylib`-[NSObject forwardingTargetForSelector:](self=0x000000010132f350, _cmd="forwardingTargetForSelector:", sel="noIMP") at NSObject.mm:2451:5
    frame #1: 0x00007fff203f7444 CoreFoundation`___forwarding___ + 225
    frame #2: 0x00007fff203f72d8 CoreFoundation`_CF_forwarding_prep_0 + 120
    frame #3: 0x00000001000033d0 KCObjcBuild`main(argc=1, argv=0x00007ffeefbff4a0) at main.m:23:9 [opt]
    frame #4: 0x00007fff20337f5d libdyld.dylib`start + 1
Copy the code

Let’s try returning an object here to receive the message and see what happens.

@implementation RYModel
- (id)forwardingTargetForSelector:(SEL)sel {
    return [[RYSubModel alloc] init];
}
@end

@implementation RYSubModel
- (void)noIMP {
    NSLog(@"%s",__func__);
}
@end
Copy the code

The output is -[RYSubModel noIMP].

5.3 The Forwarding Object is Empty

When do not achieve forwardingTargetForSelector, we found that after a dynamic resolution default returns nil, again into the dynamic resolution

// A dynamic resolution
+[RYModel resolveInstanceMethod:]--noIMP

// Print the stack information
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
  * frame #0: 0x000000010031682a libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="noIMP", cls=RYModel, behavior=2) at objc-runtime-new.mm:6536:9
    frame #1: 0x00000001002edcb9 libobjc.A.dylib`class_getInstanceMethod(cls=RYModel, sel="noIMP") at objc-runtime-new.mm:6192:5
    frame #2: 0x00007fff2040d653 CoreFoundation`__methodDescriptionForSelector + 276
    frame #3: 0x00007fff20426fa0 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 30
    frame #4: 0x00007fff203f74ef CoreFoundation`___forwarding___ + 396
    frame #5: 0x00007fff203f72d8 CoreFoundation`_CF_forwarding_prep_0 + 120
    frame #6: 0x00000001000034d0 KCObjcBuild`main + 64
    frame #7: 0x00007fff20337f5d libdyld.dylib`start + 1

// Print the variable information
(lldb) po inst
 nil
(lldb) po sel
"noIMP"

(lldb) po cls
objc[3185]: mutex incorrectly locked
objc[3185]: mutex incorrectly locked
RYModel

(lldb) po behavior
2
Copy the code
  • behavior == 2 == LOOKUP_RESOLVER
    • Meet the conditions of dynamic resolution, proceed againDynamic resolution
    • behavior ^= LOOKUP_RESOLVER
      • behaviorbecome0the

Six, summarized

This part of the understanding or need to combine debugging, simply look at the source code is difficult to clear, there are too many nested calls.

I hope my flowchart helps to understand, and I am welcome to point out any mistakes.