Is there any push in? I recently wanted to change pits.

0x00 – Run-time introduction

  • Runtime

At runtime, the code runs, it’s already loaded into memory, and only when it’s added to memory does it get activated and run

  • Buildtime

Code is dead on disk until it is loaded into memory

Objective-c Runtime Programming Guide

A set of APIS, C/C ++/ assembly, to provide runtime functionality for OC;

  • Programming interface for earlier versions: 1.0

  • The programming interface of the current version is 2.0

Earlier versions were used on Objective-C 1.0, 32-bit Mac OS X platforms

Current version: iPhone programs and 64-bit programs for Mac OS X V10.5 and later

@selector() == sel_registerName == NSSelectorFromString()Copy the code

There are three ways to use runtime

  • Call [obj someThing] from Objective-C code

  • Call NSSelectorFromString() isKindOfClass() and so on through the NSObject interface

  • Call sel_reisterName, class_getInstanceSize, etc. from the Runtime APIs

0x01 – Exploring the nature of the method

Send a message

objc_msgSend(id , sel)

Id The sel method number of the recipient of the message

Objc_superMsgSend sends a message to the parent class

Clang -rewrite-objc main.m -o main.cpp – clang -rewrite-objc main.m -o main.cpp

Understand the nature of the OC object by opening the main. CPP analysis. Again, in this file,

// Call the oc method in main
Person *p = [Person alloc];
[p say1];
[p say2];
// Clang is compiled by the underlying implementation
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say1"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say2"));
Copy the code

Objc_msgSend (say1); objc_msgSend (say1);

Person *p = [Person alloc];
objc_msgSend(p, @selector(say1));
[p say1];
// -[Person say1:]
// -[Person say1:]
Copy the code

⚠️ Build Settings -> Enable Strict Checking of objc_msgSend Calls -> NO

The final output is the same.

objc_msgSendSuperA subclass objectcallThe parent classThe method of
@interface Person : NSObject -(void)sayBye; @end @implementation Person - (void)sayBye { NSLog(@"bye bye"); } @end @interface Teacher: Person @end @implementation Teacher @end Teacher *t = [Teacher alloc]; [t say2]; struct objc_super mySuper; mySuper.receiver = t; mySuper.super_class = [Person class]; objc_msgSendSuper(&mySuper, @selector(say2));Copy the code

Sending a message to the parent requires passing a structure pointer to struct objc_super

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

The result is the output

LGPerson say : -[Person say2]
LGPerson say : -[Person say2]
Copy the code

The output shows that [t say2] and objc_msgSendSuper are both say2 method implementations of the superclass. So here’s the conclusion for now: method calls look up in their own class, and if their own class doesn’t, they look up in the inheritance chain

0x02 – Brief analysisobjc_msgSendprocess

By searching for objc_msgSend in objC-781 source code, we open the objC-MSG-arm64.s assembler file, since our target device is arm64 architecture.

The overall flow of this compilation is:

  1. Receiving the lookup method from the message first finds the class through the object ISA
  2. Through the class tocache_tCache lookup method of
  3. The cache is not there. GobitsthemethodListTo find the way

Objc_msgSend itself is written in assembly language. Why assembly? There are two main reasons:

  • OC language calls methods through objc_msgSend, which can be said to be the only way for OC method, so performance optimization on this method can improve the performance of the entire App life cycle, while assembly language performance optimization is atomic level optimization, can achieve the ultimate.

  • It is difficult for other languages to realize the function of jumping arbitrary function pointer with unknown parameters

  • Fast high efficiency + dynamic (uncertainty) parameter uncertainty

id objc_msgSend(id self, SEL _cmd, ...);
Copy the code

Now the information of the corresponding class of the object is obtained, and then the cache of the method is obtained. The function and pointer are searched according to the selector. After exception processing, the corresponding function implementation is finally jumped

0x03 – Arm64 objc_msgSend Assembler analysis

ENTRY _objc_msgSend // ENTRY UNWIND _objc_msgSend, NoFrame // No window interface CMP P0, #0 //nil check and tagged pointer check If self looks like 0 #if SUPPORT_TAGGED_POINTERS // if MSB tagged pointer looks like 0 #if SUPPORT_TAGGED_POINTERS // If the value is less than or equal to 0, the flag is redirected to LNilOrTagged. #else // SUPPORT_TAGGED_POINTERS B.e q LReturnZero is not supported #endif LDR p13, [x0] // p13 = isa // GetClassFromIsa_p16 p13 // p16 = classCopy the code

LDR is short for Load Register. [] is an indirect address, which means to fetch 8 bytes of data from the address represented by x0 and place it in x13. X0 is the address of self, so what we get here is the address of ISA

Because self is a pointer, it actually points to struct objc_object, defined as


struct objc_object {
    private:
  	isa_tisa; . }Copy the code

Objc_object has only one member isa, so fetching what the pointer points to is the value of ISA.

Call GetClassFromIsa_p16 to get the address of the class, which is important because we’re going to use this class,

To obtainclass

The realization of the GetClassFromIsa_p16

.macro GetClassFromIsa_p16 /* SRC */ #if SUPPORT_INDEXED_ISA $0 // 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__ //arm64 // 64-bit packed isa and p16, $0, #ISA_MASK // #else // 32-bit raw isa mov p16, $0 #endif .endmacroCopy the code

and p16, $0, #ISA_MASK

And denotes and, p16=$0&isa_mask, where $0 is the first parameter passed in, isa, ISA_MASK 0x0000000ffffffff8ULL

This and objC-781 source code

inline Class 
objc_object::ISA(a) 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK); // Use the and algorithm to get the class
#endif
}
Copy the code

So at this point we’ve got the address of the class, so we can go to LGetIsaDone: and cache it and look up the cache line

Since the instance methods of an object are stored in the class to which they belong, its method cache is also in the class, to recap from the previous article

struct objc_class : objc_object {
    // Class ISA; / / 8
    Class superclass;   / / 8
    cache_t cache;      // 16 // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom RR /alloc flags attribute method protocol //8
}
Copy the code

See my last article about caching in cache_t, where the register state is x1 = SEL and x16 = ISA

Lookup process

CacheLookup NORMAL|GETIMP|LOOKUP <function>

// p1 = SEL, p16 = isa
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
Copy the code

The CACHE is defined as two pointer sizes (16 bytes)

#define CACHE (2 * __SIZEOF_POINTER__)

Insert 8 bytes of data from the offset CACHE in register X16 into p11.

Why offset 16 bytes to see objc_class definition


struct objc_class : objc_object {

  // Class ISA; / / 8 bytes
 
 Class superclass;  / / 8 bytes

 cache_t cache; // formerly cache pointer and vtable

 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags. }Copy the code

The cache is located after ISA and superclass, so we offset the cache by 16 bytes and then fetch 8 bytes to get the _maskAndBuckets value.

	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
Copy the code

As mentioned in the previous article, _maskAndBuckets is 48 places lower,

P10 = p11 & 0 x0000ffffffffffff p10 = buckets

LSR means logical shift to the right, p11 moves 48 bits to the right, maks is put in P11, SEL is put in P1, sel index in the hash table is obtained by sel&mask,

P11 = p11 >> 48 pp12 = p1&p11Copy the code

Then get the corresponding address of the index entry

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

PTRSHIFT=3 PTRSHIFT= 4 PTRSHIFT= 16 PTRSHIFT= 16 PTRSHIFT= 16 PTRSHIFT= 16 PTRSHIFT PTRSHIFT= 16 PTRSHIFT PTRSHIFT= 16 PTRSHIFT PTRSHIFT PTRSHIFT= 16 PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT PTRSHIFT Get the address of the index entry and store it in p12.

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket

Copy the code

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

Here the cache table is scanned twice, as shown in the diagram

1: cmp p9, p1 // if (bucket->sel ! CacheHit $0 // call or return IMP hit the cache and call impCopy the code
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop
Copy the code

Check whether the index bucket is empty by CheckMiss $0

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
Copy the code

Check whether it is 0 in ChickMiss by p9. P9 represents sel. If it is empty, __objc_msgSend_uncached is executed.

If it is not empty, it will go to the later process

cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
Copy the code

P10 is the address of the current cache table, and P12 is the bucket of the current SEL. It means to determine whether the current P12 is the current table head, if not, it will continuously compare the table

	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop
Copy the code

LDP followed by an instruction! “, subtract a BUCKET_SIZE from p12, write it back to P12, and add IMP and SEL to P17 and P9 respectively.

If it is the header of the table, the process goes through 3

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
Copy the code

P11 is the address of the current cache. Since mask occupies 16 bits higher, move 48 bits right to obtain mask. Since each bucket is 16 bytes, mask << 4 is required to obtain the total occupied size, which is also the total offset. Add the offset to the start of the p12, so the p12 points to the end of the table

// offset = mask << 4Copy the code

The method cache is then iterated [a second time], reworking the first and second steps of the process, and the third step pops up

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

Copy the code

Overall code reference

ldp p17, p9, [x12] // {imp, sel} = *bucket 1: cmp p9, p1 // if (bucket->sel ! = _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp p12, p10 // wrap if bucket == buckets b.eq 3f ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket b 1b // loop 3: // wrap: p12 = first bucket, w11 = mask #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) // p12 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 add p12, p12, p11, LSL #(1+PTRSHIFT) // p12 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp p17, p9, [x12] // {imp, sel} = *bucket 1: cmp p9, p1 // if (bucket->sel ! = _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp p12, p10 // wrap if bucket == buckets b.eq 3f ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket b 1b // loop LLookupEnd$1: LLookupRecover$1: 3: // double wrap JumpMiss $0Copy the code

__objc_msgSend_uncached

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. Macro MethodTableLookup // Save register start // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0.. x8, q0.. q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] STR x8, [sp, #(8*16+8*8)] LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 mov x2, x16 mov x3, // IMP in x0 mov x17 // IMP in x0 mov x17 // IMP in x0 mov x17 // IMP in x0 mov x17 // IMP in x0 mov x17 // IMP in x0 mov x17 Registers and return LDP Q0, Q1, [sp, #(0*16)] LDP Q2, Q3, [sp, #(2*16)] LDP Q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 authenticatelr.endMacro. macro TailCallFunctionPointer // $0 = function pointer Value br $0 // Just call the BR directive to jump the incoming IMP .endmacroCopy the code

So I’m done here, and then I’m going to add Tagged Pointer and nil.


Refer to the link

  • Arm64 objc_msgSend source code interpretation

  • Learn about ARM64 assembler

  • ARM64 assembler – registers and instructions

  • ARM 64 assembler – Registers & instructions

  • SEL type, IMP type and Class type in iOS

  • IOS Development Master class


Welcome big brother message correction 😄, code word is not easy, feel good to give a thumbs-up 👍 have any expression or understanding error please leave a message; Common progress;