1. Runtime and compile time

  • Compile time: as the name implies, compile time is the time to do some simple translation work, such as keyword checking, lexical analysis, syntax analysis and so on.

  • Runtime: The code runs and is loaded into memory. If an error occurs, the program crashes.

2. Three ways to call Runtime

There are three ways to call Runtime:

  1. Objective-C CodeIn code, like an object method call, [user sayHello];;
  2. Framework&Service, the call to the interface, such asisKindOfClass,isMemberOfClassAnd so on;
  3. Runtime API,c/c++Use of source code methods, such asobjc_msgSend.objc_msgSendSuper.

Above:

  • CompilerFor the compiler layer, it translates code into some intermediate state language, and it also does someThe LLVM compilerFor example, willallocMethod optimization executionobjc_allocMethods.
  • runtime system libarary It’s the underlying library.

3. The nature of method calls

1. Explore the basics

Use Clang to compile oc code into C/C ++ code to see the essence.

 ((void (*)(id, SEL))(void *)objc_msgSend)((id)user, sel_registerName("sayHello"));
Copy the code

In Objective-C, the sayHello method on a User object is finally called by objc_msgSend in C/C ++. Objc_msgSend looks familiar, because we saw it in the previous chapter, in the cache_T structure analysis and the underlying exploration!

To review our thinking and process for exploring cache_t:

The function that determines what a class does is a function, so look for a cache_t function.

  1. In the cache_t structure, there is a method void incrementOccupied(); ;;;;;;;;;;;; , makes sense: Insert something to cache_t, incrementing the footprint by 1;

  2. The global search incrementOccupied() method is only used in one place, to insert data into cache_T, the cache_t::insert method.

  3. Continuing with a global search for cache_t:: the insert method found a very important comment. Read the comment: Cache_t has two points: cache read and cache write. See below:

    This gives us a great idea. In addition to the above analysis, method calls are eventually converted to message sending, i.e., objc_msgSend, which should be implemented by fetching the method from the cache cache_t via the cache_getImp method, thus completing a process of finding the method and finally completing the execution of the function.

2.OC and C/C ++ source conversion

Make a transformation of OC code and use objc/message.h c/ C ++ source code to implement method calls. Such as:

        GFPerson * user = [GFPerson alloc];
        [user sayHello];
        objc_msgSend(user, sel_registerName(@"sayHello"));
Copy the code

The result of the run discovery call is consistent. Note that objc_msgSend’s draconian checking mechanism needs to be turned off. See below:

3.objc_msgSendSuper

Introduce a case

Son inherits from Father, Son just declares sayHello, but doesn’t implement sayHello, Father implements sayHello. In main, Son calls the method sayHello. Refer to the following example:

@interface Father : NSObject
-(void)sayHello;
@end
@implementation Father
-(void)sayHello{
    NSLog(@"sayHello %s", __func__);
}
@end

@interface Son : Father
-(void)sayHello;
@end
@implementation Son
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Son *son = [[Son alloc]init];
        [son sayHello];
  }
  return 0;
}
Copy the code

Print result:

Although Son does not implement the sayHello method, there is no exception after the program runs, but instead calls the sayHello method of the parent class. Note that the recipient of the message is himself, but he has not implemented it, calling the method of the parent class. We can use objc_msgSendSuper to call methods of the superclass directly.

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
Copy the code

The method takes two arguments:

  • Parameter 1: structureobjc_superPointer;
  • Parameter 2: Method numbersel.

The objc_super structure is as follows:

/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if ! defined(__cplusplus) && ! __OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif /* super_class is the first class to search */ };Copy the code

Final implementation code:

       Son *son = [[Son alloc]init];
       [son sayHello];
       struct objc_super obj;
       obj.receiver = son;
       obj.super_class = [father class];
       objc_msgSendSuper(&obj, sel_registerName("sayHello"));
Copy the code

The printed result is:

It can also be invoked successfully.

4. Quick method search

Objc_msgSend’s fast method lookup process is implemented through assembly, which is used for its speed. Search globally for _obj_msgSend in libobjc.a.ylib, and the assembly core implementation is in the objc_msg_arm64.s file.

Here is the core code for the _objc_msgSend assembly

1. _objc_msgSend function

Unwinding _objc_msgSend, NoFrame // p0 compares to null to determine whether the recipient exists, Where p0 is the first parameter of objc_msgSend - message receiver CMP p0, #0 // nil check and tagged pointer check #if SUPPORT_TAGGED_POINTERS b.le LNilOrTagged // (MSB tagged pointer looks Negative) #else B.eflreturnZero #endif LDR p13, [x0] = isa GetClassFromIsa_p16 p13, 1 X0 // p16 = LGetIsaDone // calls imp or objc_msgSend_uncached CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached #if SUPPORT_TAGGED_POINTERS LNilOrTagged: b.eq LReturnZero // nil check GetTaggedClass b LGetIsaDone // SUPPORT_TAGGED_POINTERS #endif LReturnZero: // x0 is already zero mov x1, #0 movi d0, #0 movi d1, #0 movi d2, #0 movi d3, #0 ret END_ENTRY _objc_msgSendCopy the code

Process description:

  1. P0 = null; p0 = null; p0 = null; p0 = null;

  2. If it is not empty, get the isa pointer and place it in p13, which is the first address of the object.

  3. GetClassFromIsa_p16 isa macro defined in the implementation, using isa to find the corresponding class; ExtractISA is also a macro definition that takes the isa&isaMask passed in, gets the class, and assigns the class to p16;

    .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if ! needs_auth */ #if SUPPORT_INDEXED_ISA // Indexed isa mov p16, \src // optimistically set dst = src tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa // isa in p16 is indexed adrp x10, _objc_indexed_classes@PAGE add x10, x10, _objc_indexed_classes@PAGEOFF ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 1: #elif __LP64__ .if \needs_auth == 0 // _cache_getImp takes an authed class already mov p16, \src .else // 64-bit packed isa ExtractISA p16, \src, \auth_address .endif #else // 32-bit raw isa mov p16, \ SRC # endif.endMacro ExtractISA. Macro ExtractISA and $0, $1, # isa_mask.endMacroCopy the code
  4. Class is found, enter the macro CacheLookUp CacheLookUp process.

2. Macro CacheLookUp

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant mov x15, x16 // stash the original isa LLookupStart\Function: // p1 = SEL, p16 = isa #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS ldr p10, [x16, #CACHE] // p10 = mask|buckets lsr p11, p10, #48 // p11 = mask and p10, p10, #0xffffffffffff // p10 = buckets and w12, w1, w11 // x12 = _cmd & mask #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 ldr p11, [x16, #CACHE] // p11 = mask|buckets #if CONFIG_USE_PREOPT_CACHES #if __has_feature(ptrauth_calls) tbnz p11, #0, LLookupPreopt\Function and p10, p11, #0x0000ffffffffffff // p10 = buckets #else and p10, p11, #0x0000fffffffffffe // p10 = buckets tbnz p11, #0, LLookupPreopt\Function #endif eor p12, p1, p1, LSR #7 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask #else and p10, p11, #0x0000ffffffffffff // p10 = buckets and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHES #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 ldr  p11, [x16, #CACHE] // p11 = mask|buckets and p10, p11, #~0xf // p10 = buckets and p11, p11, #0xf // p11 = maskShift mov p12, #0xffff lsr p11, p12, p11 // p11 = mask = 0xffff >> p11 and p12, p1, p11 // x12 = _cmd & mask #else #error Unsupported cache mask storage for ARM64. #endif add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // do { 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- cmp p9, p1 // if (sel ! = _cmd) { b.ne 3f // scan more // } else { 2: CacheHit \Mode // hit: call or return imp // } 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; cmp p13, p10 // } while (bucket >= buckets) b.hs 1b // wrap-around: // p10 = first bucket // p11 = mask (and maybe other bits on LP64) // p12 = _cmd & mask // // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION. // So stop when we circle back to the first probed bucket // rather than when hitting  the first bucket again. // // Note that we might probe the initial bucket twice // when the first probed slot is the last entry. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS add p13, p10, w11, UXTW #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p13 = buckets + (mask << 1+PTRSHIFT) // see comment about maskZeroBits #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 add p13, p10, p11, LSL #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket // do { 4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- cmp p9, p1 // if (sel == _cmd) b.eq 2b // goto hit cmp p9, #0 // } while (sel ! = 0 && CCMP p13, p12, #0, ne // bucket > first_probed) b.h4b...... Omit. EndmacroCopy the code

Only the CACHE_MASK_STORAGE_HIGH_16 environment is analyzed:

  1. After obtaining the address of the class object, we shift the pointer by 16 bytes to get the first address of cache_t; Because in objc_class, isa is 8 bytes, superclass is 8 bytes; To get the first address of cache_t, shift it by 16 bytes and assign it to p11, which is _bucketsAndMaybeMask; The source code is as follows:

    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
            ldr	p11, [x16, #CACHE]  // p11 = mask|buckets
    Copy the code
  2. In an ARM64 environment, masks and buckets together take up 8 bytes, 64 bits. Mask is in the top 16 and Buckets are in the bottom 48. The high 16 bits are zeroed by the mask operation &# 0x0000FFFFFFFFFE to get the buckets; Assign buckets to P10. Cache_t structure analysis and underlying exploration

    #if CONFIG_USE_PREOPT_CACHES #if __has_feature(ptrauth_calls) tbnz p11, #0, LLookupPreopt\Function and p10, p11, P10 = buckets #else P10, p11, p10, p11 P10 = buckets TBNZ p11, #0, LLookupPreopt\Function #endif LSR #7 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask #else and p10, p11, #0x0000ffffffffffff // p10 = buckets and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHESCopy the code
  3. When a cache is inserted, it is stored in the form of a hash index, and the algorithm for the index is:

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        uintptr_t value = (uintptr_t)sel;
    #if CONFIG_USE_PREOPT_CACHES
        value ^= value >> 7;
    #endif
        return (mask_t)(value & mask);
    }
    Copy the code
    • Assembly implementation using the same algorithm, will now_cmdMoves to the rightseven, and the XOR operationeorAssigned top12;
    • p11Moves to the right48A gainmaskAnd thenp12andmaskthroughandGet the subscript and assign it top12And that’s it_cmdThe subscript!

    At this point:

    • p11Is equal to the_bucketsAndMaybeMask;
    • p10Is equal to thebucketsWhich is the first onebucket_tAddress;
    • p12Equal to the method we’re looking forThe hash index.
  4. We’ve got the hash subscript, we’ve got the address of the first element of buckets, so how do we find the location of _cmd? Yes, the address is translated, by how much? A multiple of 16 bytes, because the two attributes in bucket_t are the imp and SEL pointer addresses, 8+8=16 bytes.

       add	p13, p10, p12, LSL #(1+PTRSHIFT)
       // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    Copy the code

    P13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) where PTRSHIFT = 3; P13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)); (_cmd & mask) << (1+PTRSHIFT) = (1+PTRSHIFT) You get the bucket address of the current _cmd.

  5. Open a loop where [x13] takes the value in register x13. P17 points to an address. This instruction assigns a value to the address, and LDP is an out-stack instruction. P17 = IMP, p9=sel, p13 = SEL, p17= IMP, p9=sel

    // do { 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- cmp p9, p1 // if (sel ! = _cmd) { b.ne 3f // scan more // } else { 2: CacheHit \Mode // hit: call or return imp // } 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; cmp p13, p10 // } while (bucket >= buckets) b.hs 1bCopy the code
    • cmp p9, p1, if the currently obtainedselAnd what to look forselIf yes, the cache hits,CacheHit;
    • If they are not equal, enterThree processes, to determine the currently obtainedsel.p9Is it null? If it is, thenMiss, the cache is not hit;
    • If you getselIf the value is not empty, subscript conflicts exist. Is currently obtainedbucketThe address with the firstbucketOf addresses;
    • If the address is greater than or equal to the first address, continue the comparison process, look forward, and loop!
    • Until the first address is found.
  6. If not found at the end of the above loop, we will go to the following process: in CACHE_MASK_STORAGE_HIGH_16, p11 is also moved 48 bits to the right to obtain the mask, which is equal to the total space allocated minus 1, so we will obtain the location of the last storage space based on the first address. Add mask*16, so p13 is the current largest storage space, which is also the last storage space.

        #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
            add	p13, p10, w11, UXTW #(1+PTRSHIFT)
                                                    // p13 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
            add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                                                    // p13 = buckets + (mask << 1+PTRSHIFT)
                                                    // see comment about maskZeroBits
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
            add	p13, p10, p11, LSL #(1+PTRSHIFT)
                                                    // p13 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
         
    Copy the code
  7. Reset the value of p12. As already known above, p12 is the storage subscript of the method _cmd to be searched. Add index*16 to the first address to obtain the bucket address corresponding to the method _cmd to be searched and assign the value to p12.

      add	p12, p10, p12, LSL #(1+PTRSHIFT)
      // p12 = first probed bucket
    Copy the code
  8. Open another loop, this time from the last position, to the _cmd to find the corresponding position, forward search.

    // do { 4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- cmp p9, p1 // if (sel == _cmd) b.eq 2b // goto hit cmp p9, #0 // } while (sel ! = 0 && ccmp p13, p12, #0, ne // bucket > first_probed) b.hi 4bCopy the code
    • cmp p9, p1, if the currently obtainedselAnd what to look forselSimilarly, jump to process 2, that is, cache hit,CacheHit;
    • If not, judgeselWhether it is null, if it is not, and the address obtained by the loop is greater thanp12To continue the process.
  9. If all the above processes fail to hit the cache, the MissLabelDynamic process enters and fails to hit the cache.

3. The CacheHit is hit

In CacheLookup, if Mode is passed to NORMAL, TailCallCachedImp is executed. See the source code below:

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa .macro CacheHit .if $0 == NORMAL TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp .elseif $0 == GETIMP mov p0, p17 cbz p0, 9f // don't ptrauth a nil imp AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP 9: ret // return IMP .elseif $0 == LOOKUP // No nil check for ptrauth: the caller would crash anyway when they // jump to a nil IMP. We don't care if that jump also fails ptrauth. AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP cmp x16, x15 cinc x16, x16, ne // x16 += 1 when x15 ! = x16 (for instrumentation ; Fallback to the parent class) ret // Return IMP via x17.else. abort Oops.endif.endMacro // Call IMP TailCallCachedImp // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa eor $0, $0, $3 br $0 .endmacroCopy the code

In the implementation of TailCallCachedImp, bit xOR operation is performed to obtain IMP. When storing the IMP, the IMP is encoded, as shown in the following figure:

When the fetch executes the call, it needs to be decoded.

4. Static function __objc_msgsend_cached

If the cache does not hit, the MissLabelDynamic process will be entered. A global search for MissLabelDynamic shows that MissLabelDynamic is the third parameter of CacheLookUp:

__objc_msgsend_cached from _objc_msgSend. See below:

Search globally for __objc_msgsend_cached, and get the following definition:

        STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
Copy the code

Process description: Execute macro MethodTableLookup in this function to continue tracking MethodTableLookup. In the assembly implementation of MethodTableLookup, we can see that the most important method is _lookUpImpOrForward, and then a global search for _lookUpImpOrForward fails, which indicates that the method is not an assembly implementation. Need to go to C/C++ source search.

5. 5

In C/C ++, to find the assembly, need to search for the method to add an underscore. When you call a C/C ++ method in assembly to find a C/C ++ method, you need to remove an underscore from the method you want to find.

5. Slow method search

The slow method search process lookUpImpOrForward