In the last article, we combined objC source code analysis of objc_msgSend process, today we use a real machine, through the assembly process analysis of objc_msgSend

objc_msgSendReal machine assembly process

Cache miss

Create a new project and create a new class Person in the project:

@interface Person : NSObject
- (void)func1;
- (void)func2;
- (void)func3;
- (void)func4;
@end

@implementation Person
- (void)func1 {
    NSLog(@"%s", __func__);
}
- (void)func2 {
    NSLog(@"%s", __func__);
}
- (void)func3 {
    NSLog(@"%s", __func__);
}
- (void)func4 {
    NSLog(@"%s", __func__);
}
@end
Copy the code

In main, the breakpoint is as follows:

Enter the assembly window:

Debug->Debug Workflow->Always Show Disassembly

Enter the objc_msgSend:

To continue:

By LLDB print, verify the objc_msgSend process for object method func3, currently of the Person class:

x16=isa

x11=cache

To determine whether position 0 of w11 is not 0 is to determine whether preopt_cache_t is present in cache_t

x10=buckets

X12 = the first location retrieved

x13=bucket_t

x17=imp x9=sel

X9 and X1 are not equal, jump to instruction 0x1B01E3210

Jumps to _objc_msgSend_uncached, and checks whether X9 is 0. If yes, continue to _objc_msgSend_uncached

Cached not hit, skip _objc_msgSend_uncached

A cache hit

The main method code is modified as follows:

Breakpoint execution, enter the assembly window, final result:

At this point, the x10 = buckets, x16 = isa

buckets=buckets ^ _cmd,bucket=bucket ^ isa

The assembly cache

Cached cached = __objc_msgSend_uncached; cached cached = __objc_msgSend_uncached.

__objc_msgSend_uncached

Objc – msG-arm64.s objc-msg-arm64.s

It mainly executes two methods: MethodTableLookup and TailCallFunctionPointer

TailCallFunctionPointer

A direct jump to $0, x17, indicates that the important operation is on the MehtodTableLookup

MehtodTableLookup

After _lookUpImpOrForward is jumped, x0, or IMP, is assigned to X17. X0 is the first register that is often used to receive the return value, so IMP is assigned to x0 register in _lookUpImpOrForward

lookUpImpOrForward

It turns out that we have jumped to C++ lookUpImpOrForward:

So why was the assembly lookup cache used before? That’s because assembly language is closer to machine language, very efficient and relatively safe; Assembly is a quick lookup process, while lookUpImpOrForward is a slow lookup process that iterates through methodList.

Slow search process

lookUpImpOrForward

Since lookUpImpOrForward ultimately needs to return an IMP, a look at the method implementation captures its core:

In addition, we need to pay attention to two methods checkIsKnownClass and realizeAndInitializeIfNeeded_locked

checkIsKnownClass

This method determines whether the current class is already registered with the current cache table allocatedClasses.

realizeAndInitializeIfNeeded_locked

Finally, the parent and metaclass are initialized again in the method, and the data is manipulated against RW and RO, because there are methodLists in ro and RW;

Binary search process

Next, we focus on how to find a method in lookUpImpOrForward. We find that an IMP lookup loop has no end condition, so we find imp because there must be a condition inside the for loop to break out of the FOR loop. So this is the heart of what needs to be studied and analyzed; Before starting our analysis, let’s briefly outline the search process:

  • 1. Look up yours firstmethodListThrough theselfindimp
  • 2, if their ownmethodListIf you can’t find it, you need to look it up based on past experienceThe parent classorNSObjectBecause theNSObjecttheThe parent classisnil, so if we can’t find it, we need to jump out of the current search process;

Next, we analyze the source code in detail:

This brings us to our core search process:

Let’s analyze this loop with an example:

Count >>= 1

If count = 8, then the binary expression of 8 is 1000, then 8 >> 1 is 1000, which is the decimal 4, then we can know that count >> 1 is halved (divided by 2). If count >>= 1, count = count >> 1;

So if at this point, there are 8 items of data in the list, and the 6th item is the data we are looking for, the flow analysis is as follows:

First loop:

Count = 8, base = 0, count >> 1 = 4, probe = 0 + (4) = 4, keyValue > proValue So base = 4 + 1 = 5, count– = 7,

First loop result: base = 5, count = 7

Second loop:

Count = 7, count >>= 1, count = 3, base = 5, 3 >> 1 = 1, probe = 5 + 1 = 6,

This is the binary search process

Slow lookup recursive process

After the binary lookup is done and meth is found, goto done is executed

After finding imp according to meth, execute goto done

Done cache fills log_AND_fill_cache

Then call insert to cache:

But what if we didn’t find the IMP in the previous flow?

If meth is not currently found, point curClass to the parent class and continue looking for IMP in the parent class

Assembles a quick lookup flow from the parent class, so we’re back to a similar flow as before:

If it’s not found, it’s going to go through the slow search for the parent, and if it’s not found, it’s going to keep looking for the parent, so it’s a recursive search, and when curClass is nil, that means we’ve found all of our parent classes, we haven’t found any, so we’re going to assign forward_IMP to the IMP