# 11.源码解读objc\_msgSend

### 前言

* 本文基于
  * `objc4-818.2`源码
  * `iOS14.6 iPhoneX`
* 源码版本、系统版本、设备不同的话，真机调试的汇编会和源码有不同。自行调试的时候需要注意
* 源码中对于重要部分做了注释，`...`表示省略了一些其他架构或者无关紧要的代码
* 为了方便阅读，汇编代码中的做了`缩进调整`和`分块注释`，阅读起来应该会轻松不少
* `电脑端`阅读体验更佳哦
* 本文看着会比较累，用了个看着比较轻松愉悦的主题。
  * 配上一杯 ☕️ 体验更佳
* `objc_msgSend` 相关的内容很多，此文会持续更新、修正
  * 具体更新内容可以查看`更新记录`

> 欢迎收藏、交流、抓虫

#### 更新记录

|   更新日期   |  变更  |
| :------: | :--: |
| 20210701 | 首次发布 |

### 一、平时我们是如何调用方法的？

有三种方式进行调用： `OC Method`

```C++
[model doSomething]
```

`NSObject API`

```C++
[model isKindOfClass:[NSString class]]
```

`objc API`。

```C++
class_getSuperclass([RYModel class])
```

API的层级结构如下：

![runtimeLevel.png](/files/-MdS_mN4c7SI2ELpWQ9S)

#### 1.1 实验三种方式

```C++
RYModel *obj = [[RYModel alloc] init];
[obj dosomethingA];

[obj performSelector:NSSelectorFromString(@"dosomethingA")];

// < Xcode 12
//    objc_msgSend((id)obj, sel_registerName("dosomethingA"));

// Xcode 12 需要转类型，只改Build setting不行
((void (*)(id, SEL))(void *)objc_msgSend)(obj, sel_registerName("dosomethingA"));
```

需要注意的点：

* `#import <objc/message.h>`
* `Xcode 12` 以下
  * `Build setting` -> `objc_msgSend` -> `Enable Strict Checking of objc_msgSend Calls` -> `NO`
* `Xcode 12` 以上
  * 需要对 objc\_msgSend 转类型，才能正常调用

![msgsend01.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3562e316364d4808ba1145bfee9909b8~tplv-k3u1fbpfcp-watermark.image)

输出：都能够正常调用

```C++
ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]
ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]
ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]
```

#### 1.2 调用父类方法

```C++
@interface RYSubModel : RYModel

- (BOOL)isSub;

@end

@implementation RYSubModel

- (BOOL)isSub {
    NSLog(@"%s",__FUNCTION__);
    [super dosomethingA];
    return  YES;
}

@end
```

`RYSubModel` 继承 `RYModel` ，我们看看C++文件中 `isSub` 的情况。

```C++
static BOOL _I_RYSubModel_isSub(RYSubModel * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_py_7v1wvf813z5bmw0hr97yplwc0000gn_T_RYModel_c52934_mi_6,__FUNCTION__);
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("RYSubModel"))}, sel_registerName("dosomethingA"));
    return ((bool)1);
}
```

这里 `[super dosomethingA]` 转成C++后，发现一个 `objc_msgSendSuper` 方法，通过它来调用父类的方法。

#### 1.3 objc\_msgSendSuper

我们试试能不能直接调用 `objc_msgSendSuper`。

* `objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)`
  * super
    * objc\_super 结构体指针
  * op
    * selector

```C++
/// 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 */
};
```

***objc\_super***

```C++
/// 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 */
};
```

***代码***

```C++
struct objc_super ry_super;
ry_super.receiver = obj;
ry_super.super_class = [RYModel class];
((void (*)(id, SEL))(void *)objc_msgSendSuper)((__bridge id)(&ry_super), sel_registerName("dosomethingA"));

输出：
ObjcMsgSend[17330:6265149] -[RYModel dosomethingA] // 调用父类方法成功
```

***修改父类参数***

我们将父类修改成它自己的类对象试试。 `ry_super.super_class = [RYSubModel class];`，输出依然正确。内部有个查找的链条。

#### 1.4 源码

我们找一下 `objc_msgSend` 的源码。

ARM64版的源码都在 `objc-msg-arm64.s` 中，是 `汇编` 的。

通过添加符号断点也可以查看到汇编代码。

### 二、汇编说明

#### 2.1 这里用到的主要的的汇编指令备注

|  指令  |                  示例                  |                                                           备注                                                          |
| :--: | :----------------------------------: | :-------------------------------------------------------------------------------------------------------------------: |
|  cmp |             cmp x0, #0x0             |                                              作差值计算，结果丢弃，只调整状态寄存器`CPSR`标志位                                             |
| b.le |           b.le 0x1afad2b98;          |                                意为`less than or equal to`，结果`<=0`，的时候跳转指令 `0x1afad2b98`                                |
| b.eq |           b.eq 0x1afad2b68           |                                          意为`equal`，结果`==0`，时跳转指令`0x1afad2b68`                                         |
|  and |             and x16, x13             |                                                     x16 & x13 与操作                                                     |
|  ldr |            ldr x13, \[x0]            |                                                 将地址x0的储存器的数据，放入x13寄存器                                                 |
|  lsr |                lsr #48               |                                                        逻辑右移48位                                                        |
|  lsl |                lsl #4                |                                                         逻辑左移4位                                                        |
|  ldp |          ldp x17, x9, \[x12]         |                                                    取出x12的值存入x17,x9                                                    |
| b.ne |           b.ne 0x1afad2b54           |                                                 不相等，跳转指令`0x1afad2b54`                                                 |
| tbnz | tbnz p11, #0, LLookupPreopt\Function |                          `Test and branch Not zero`，即：如果p11的第0位不为0就执行`LLookupPreopt\Function`                         |
|  TBZ |                   -                  |                                                      为0，和`tbnz`类似                                                     |
| adrp |              adrp x0, 1              | 1. 将1的值,左移12位 `1 0000 0000 0000 == 0x1000`;\n 2.将PC寄存器的低12位清零 `0x1045228b0 ==> 0x104522000`; 3.将将1 和 2 的结果相加 给 X0 寄存器 |
|  eor |                   -                  |                                                           异或                                                          |

#### 2.2 寄存器说明

[寄存器](/wiki/ni-xiang/01.-ji-cun-qi.md)

* `x0-x7`，是用来传参的。
  * x0：消息接收者
  * x1：sel

### ☕️ 休息一下吧，后面开始会有点累

### 三 \_objc\_msgSend 源码汇编

```C++
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

// 判断是否存在消息接收者
cmp	p0, #0			// nil check and tagged pointer check 比较P0寄存器和0

#if SUPPORT_TAGGED_POINTERS 
// 支持Taggedpointer ARM64下 SUPPORT_TAGGED_POINTERS = 1，
// 根据状态寄存器结果 p0 - 0 <= 0 跳转执行 LNilOrTagged
b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
// 等于0，即接收者为nil，LReturnZero
b.eq	LReturnZero
#endif

// ISA 存入p13
ldr	p13, [x0]		// p13 = isa

// 从ISA中获取 Class，p16存的是Class的指针
GetClassFromIsa_p16 p13, 1, x0	// p16 = class

// 获取ISA完成后执行：
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 调用IMP或者查缓存
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

END_ENTRY _objc_msgSend
```

#### 3.1 GetClassFromIsa\_p16

获取Class存入p16

```C++
/********************************************************************
 * GetClassFromIsa_p16 src, needs_auth, auth_address
 * src is a raw isa field. Sets p16 to the corresponding class pointer.
 * The raw isa might be an indexed isa to be decoded, or a
 * packed isa that needs to be masked.
 *
 * On exit:
 *   src is unchanged
 *   p16 is a class pointer
 *   x10 is clobbered
 ********************************************************************/
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
...
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else // needs_auth == 1
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
```

#### 3.2 ExtractISA 获取ClassISA

```C++
// XS/XS Max/XR A12处理器开始
#if __has_feature(ptrauth_calls)
// JOP
...
.macro ExtractISA
    // ISA($1) & ISA_MASK 存入 p16p16($0)，即 class
	and	$0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
	xpacd	$0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
	mov	x10, $2
	movk	x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
	autda	$0, x10
#endif
.endmacro
...

// JOP
#else
// not JOP

...
.macro ExtractISA
    // ISA($1) & ISA_MASK 存入 p16($0)，即 class
	and    $0, $1, #ISA_MASK
.endmacro

// not JOP
#endif
```

#### 3.3 CacheLookup 查找缓存

参数：

* Mode：缓存命中模式
  * CacheHit
* Function
  * 命中缓存后调用
* MissLabelDynamic
  * 未命中调用
* MissLabelConstant
  * MissLabelConstant is only used for the GETIMP variant

***官方注释***

* 建议查看一下，有助于理解

```C++
/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
 *
 * MissLabelConstant is only used for the GETIMP variant.
 *
 * Locate the implementation for a selector in a class method cache.
 *
 * When this is used in a function that doesn't hold the runtime lock,
 * this represents the critical section that may access dead memory.
 * If the kernel causes one of these functions to go down the recovery
 * path, we pretend the lookup failed by jumping the JumpMiss branch.
 *
 * Takes:
 *	 x1 = selector
 *	 x16 = class to be searched
 *
 * Kills:
 * 	 x9,x10,x11,x12,x13,x15,x1
 *
 * Untouched:
 * 	 x14
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *                  In LOOKUP mode, the two low bits are set to 0x3
 *                  if we hit a constant cache (used in objc_trace)
 *          (not found) jumps to LCacheMiss
 *                  with x15 = class
 *                  For constant caches in LOOKUP mode, the low bit
 *                  of x16 is set to 0x1 to indicate we had to fallback.
 *          In addition, when LCacheMiss is __objc_msgSend_uncached or
 *          __objc_msgLookup_uncached, 0x2 will be set in x16
 *          to remember we took the slowpath.
 *          So the two low bits of x16 on exit mean:
 *            0: dynamic hit
 *            1: fallback to the parent class, when there is a preoptimized cache
 *            2: slowpath
 *            3: preoptimized cache hit
 *
 ********************************************************************/
```

***源码***

```C++
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

    // 将x16(class)移动到x15
	mov	x15, x16			// stash the original isa

LLookupStart\Function:
...


.endmacro

```

源码较长，我们分部分看

#### 3.4 LLookupStart\Function 开始查找

* 获取当前 bucket\_t 和 HashIndex，用来查找缓存
  * 这里对源码的`if-else`缩进进行调整，方便查看

```C++
LLookupStart\Function:
	// p1 = _cmd, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
// ARM64 && __LP64__: MacOS 或 模拟器
...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// ARM64 && __LP64__
    // ClassISA + #CACHE(0x10) 存入 p11，即cache_t(p11)
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
    #if CONFIG_USE_PREOPT_CACHES
        // 真机
        #if __has_feature(ptrauth_calls)
        // > X 的机型
            // 判断 cache_t(p11) 第0位是否不为0，如果为0，是否有共享缓存（first_shared_cache_sel）
            // 没缓存：继续走
            // 有缓存：跳转 LLookupPreopt\Function
            tbnz	p11, #0, LLookupPreopt\Function
            // cache_t(p11) & #0x0000ffffffffffff -> p10(buckets)
            // 取低 48 位
            and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
        #else
            and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
            tbnz	p11, #0, LLookupPreopt\Function
        #endif

        // (_cmd ^ _cmd >> 7) -> p12
        eor	p12, p1, p1, LSR #7
        // ((cache_t & p12) >> 48) -> p12(HashIndex)
        and	p12, p12, p11, LSR #48		// x12 = (_cmd(p1) ^ (_cmd(p1) >> 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
// ARM64 && !__LP64__：真机
略
#else
#error Unsupported cache mask storage for ARM64.
#endif
    // ((buckets + HashIndex) << 1+PTRSHIFT) -> p13(bucket_t)
    // 通过偏移找到 bucket_t 存入p13
    // p12(HashIndex)
    // `buckets`是`buckets`的首地址，HashIndex为Hash表的下标
    // 如：HashIndex为1那就偏移1个`bucket_t`的长度，即：16（sel8+imp8）
    // 左移`(1+PTRSHIFT))`是为了到`HashIndex-1`位置，这里和cache_t插入缓存的逻辑保持一致，否则找不到的。
    // 此时p13存的的是目标bucket_t地址
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// #define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE

// 遍历缓存

// 第一次循环结束后

```

#### 3.5 循环遍历（上）

```C++
						// do {
// #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__) 即 (sel + imp 指针的大小)
// 取出x13 放入 p17(imp) p9(sel)
// 然后 `*bucket--` 向前移动2个指针的长度
// 即：一个bucket_t的长度
// *bucket--: 先取值，再--
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
    // 对比 p1(传入的sel) 和 p9(p13的sel)
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
// 命中调用 CacheHit \Mode
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
    // 未命中 回到 1 继续循环
	b.hs	1b

```

#### 3.6 CacheHit

```C++
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    // 调用缓存IMP
	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
```

#### 3.7 TailCallCachedImp 执行缓存IMP

```C++
#if __has_feature(ptrauth_calls)
// JOP
...

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // (addressOfCachedIMP ^ SEL) -> addressOfCachedIMP
	eor	$1, $1, $2	// mix SEL into ptrauth modifier
    // (addressOfCachedIMP ^ isa) -> addressOfCachedIMP
	eor	$1, $1, $3  // mix isa into ptrauth modifier
    // 这个指令还没搞明白
	brab	$0, $1
.endmacro

// JOP
#else
// not JOP
...

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // (cachedIMP ^ isa) -> cachedIMP
	eor	$0, $0, $3
    // 跳转执行 cachedIMP
	br	$0
.endmacro

...

// not JOP
#endif
```

#### 3.8 循环遍历（下）

* 当循环回到第一个bucket的时候就停止。
* 将p13移到 buckets + (mask << 1+PTRSHIFT)
* 继续遍历，直到遍历完
* 如果仍然没有命中，\_objc\_msgSend\_uncached

```C++
// 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.

...
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
...

	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.hi	4b
```

#### 3.9 LLookupPreopt\Function: 查找共享缓存内的数据

```C++
#if CONFIG_USE_PREOPT_CACHES
...
    LLookupPreopt\Function:
    #if __has_feature(ptrauth_calls)
        // cache_t(p11) & #0x007ffffffffffffe 存入 p10
        and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
        // 验证？没找到这个指令的具体作用
        autdb	x10, x16			// auth as early as possible
    #endif
        // 查找共享缓存内的数据
        // x12 = (_cmd - first_shared_cache_sel)
        adrp	x9, _MagicSelRef@PAGE
        ldr	p9, [x9, _MagicSelRef@PAGEOFF]
        sub	p12, p1, p9

        // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
    #if __has_feature(ptrauth_calls)
        // bits 63..60 of x11 are the number of bits in hash_mask
        // bits 59..55 of x11 is hash_shift

        lsr	x17, x11, #55			// w17 = (hash_shift, ...)
        lsr	w9, w12, w17			// >>= shift

        lsr	x17, x11, #60			// w17 = mask_bits
        mov	x11, #0x7fff
        lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
        and	x9, x9, x11			// &= mask
    #else
        // bits 63..53 of x11 is hash_mask
        // bits 52..48 of x11 is hash_shift
        lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
        lsr	w9, w12, w17			// >>= shift
        and	x9, x9, x11, LSR #53		// &=  mask
    #endif

        ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
        cmp	x12, w17, uxtw

    .if \Mode == GETIMP
    ...
    .else
        b.ne    5f                // cache miss
        sub    x17, x16, x17, LSR #32        // imp = isa - imp_offs

    .if \Mode == NORMAL
        br	x17
    .elseif \Mode == LOOKUP
    ...

    5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
        add	x16, x16, x9			// compute the fallback isa
        b	LLookupStart\Function		// lookup again with a new isa
    .endif
#endif // CONFIG_USE_PREOPT_CACHES
```

#### 3.10 GetTaggedClass

* if SUPPORT\_TAGGED\_POINTERS

```C++
// Look up the class for a tagged pointer in x0, placing it in x16.
.macro GetTaggedClass

	and	x10, x0, #0x7		// x10 = small tag
	asr	x11, x0, #55		// x11 = large tag with 1s filling the top (because bit 63 is 1 on a tagged pointer)
	cmp	x10, #7		// tag == 7?
	csel	x12, x11, x10, eq	// x12 = index in tagged pointer classes array, negative for extended tags.
					// The extended tag array is placed immediately before the basic tag array
					// so this looks into the right place either way. The sign extension done
					// by the asr instruction produces the value extended_tag - 256, which produces
					// the correct index in the extended tagged pointer classes array.

	// x16 = _objc_debug_taggedpointer_classes[x12]
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ldr	x16, [x10, x12, LSL #3]

.endmacro
```

#### 3.11 LNilOrTagged

```C++
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq	LReturnZero		// nil check
GetTaggedClass
b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
```

#### 3.12 LReturnZero

```C++
LReturnZero:
// x0 is already zero
mov	x1, #0
movi	d0, #0
movi	d1, #0
movi	d2, #0
movi	d3, #0
ret
```

### ☕️ 看到这里，再休息一下吧。真机汇编看着更带劲儿\~

### 四、真机汇编调试

个人觉得看符号断点的汇编更直观，但是源码中有注释。

真机符号断点的作为参考，和源码对照着再看一遍会更加清晰。

```C++
libobjc.A.dylib`objc_msgSend:
/*** Start
判断接收者是否为空 
***/
->  0x197b05140 <+0>:   cmp    x0, #0x0                  ; =0x0 
    0x197b05144 <+4>:   b.le   0x197b051e8               ; <+168>
/***************************/

/*** Start
将接收者存入 x13 
即：x13 = isa 
***/
    0x197b05148 <+8>:   ldr    x13, [x0]
/***************************/

/*** Start
GetClassFromIsa_p16 -> 
ExtractISA: 
isa & ISA_MASK -> x16
即：x16 = class的isa 
***/
    0x197b0514c <+12>:  and    x16, x13, #0xffffffff8
/***************************/

/*** Start
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
查找缓存
***/
    0x197b05150 <+16>:  mov    x15, x16 // LGetIsaDone

    /*** Start
    LLookupStart\Function:
    ClassISA + #CACHE(0x10) 存入 p11，即cache_t(p11)
    判断 cache_t 第0位是否不为0，如果为0，是否有共享缓存。有就跳转 LLookupPreopt
    ***/
    0x197b05154 <+20>:  ldr    x11, [x16, #0x10]
    0x197b05158 <+24>:  and    x10, x11, #0xfffffffffffe
    0x197b0515c <+28>:  tbnz   w11, #0x0, 0x197b051b0    ; <+112>

        /*** Start
        计算 cache_hash 
        (sel ^ sel >> 7) -> p12
        ((cache_t & p12) >> 48) -> p12(HashIndex)
        ***/
        0x197b05160 <+32>:  eor    x12, x1, x1, lsr #7
        0x197b05164 <+36>:  and    x12, x12, x11, lsr #48
        /***************************/

    /***************************/

    /*** Start
    通过偏移找到 bucket_t 存入x13
    x12(HashIndex)
    `buckets`是`buckets`的首地址，HashIndex为Hash表的下标
    如：HashIndex为1那就偏移1个`bucket_t`的长度，即：16（sel8+imp8）
    左移`(1+PTRSHIFT))`是为了到`HashIndex-1`位置，这里和cache_t插入缓存的逻辑保持一致，否则找不到的。
    (buckets + HashIndex << 1+PTRSHIFT) -> p13(bucket_t)
    此时x13存的的是目标bucket_t地址
    ***/
    0x197b05168 <+40>:  add    x13, x10, x12, lsl #4
    /***************************/

    /*** Start
    遍历查缓存
    #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__) 即 (sel + imp 指针的大小)
    取出 x13 - 0x10 放入 p17(imp) p9(sel)
    0x10 = 2，即2个单位
    `*bucket--` 向前移动2个指针的长度（sel + imp）
    即：前移一个bucket_t的长度，将x13指向上一个缓存
    对比缓存和_cmd
    ***/
    0x197b0516c <+44>:  ldp    x17, x9, [x13], #-0x10
    0x197b05170 <+48>:  cmp    x9, x1
    0x197b05174 <+52>:  b.ne   0x197b05180               ; <+64>

        /*** Start
        命中缓存 TailCallCachedImp
        (cachedIMP ^ isa) -> cachedIMP
        ***/ 
        0x197b05178 <+56>:  eor    x17, x17, x16
        0x197b0517c <+60>:  br     x17
        /***************************/

    /*** Start
    while 循环条件判断
    if (sel(x9) == 0) goto Miss;
    while (bucket >= buckets) 满足条件回到 +44 指令行继续循环
    ***/
    0x197b05180 <+64>:  cbz    x9, 0x197b054c0           ; _objc_msgSend_uncached
    0x197b05184 <+68>:  cmp    x13, x10// 
    0x197b05188 <+72>:  b.hs   0x197b0516c               ; <+44>
    /***************************/


    /*** Start
    当循环回到第一个bucket的时候就停止循环后：
    
    * 将p13移到 buckets + (mask << 1+PTRSHIFT)
    * 继续遍历，直到遍历完
    * 如果仍然没有命中，_objc_msgSend_uncached
    ***/
    0x197b0518c <+76>:  add    x13, x10, x11, lsr #44
    0x197b05190 <+80>:  add    x12, x10, x12, lsl #4

        /*** Start
        第二次遍历
        ***/
        0x197b05194 <+84>:  ldp    x17, x9, [x13], #-0x10

            /*** Start
            命中缓存 跳转 56指令行：TailCallCachedImp
            ***/ 
            0x197b05198 <+88>:  cmp    x9, x1
            0x197b0519c <+92>:  b.eq   0x197b05178               ; <+56>
            /***************************/
        
        // 循环条件判断
        0x197b051a0 <+96>:  cmp    x9, #0x0                  ; =0x0 
        0x197b051a4 <+100>: ccmp   x13, x12, #0x0, ne
        //     bucket > first_probed)
        0x197b051a8 <+104>: b.hi   0x197b05194               ; <+84>
        /***************************/

        0x197b051ac <+108>: b      0x197b054c0               ; _objc_msgSend_uncached
    /***************************/

    /*** Start
    LLookupPreopt\Function
    查找共享缓存内的数据
    ***/

    // x12 = (_cmd - first_shared_cache_sel)
    0x197b051b0 <+112>: adrp   x9, 227000
    0x197b051b4 <+116>: add    x9, x9, #0x47b            ; =0x47b 
    0x197b051b8 <+120>: sub    x12, x1, x9

    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    0x197b051bc <+124>: lsr    x17, x11, #48
    0x197b051c0 <+128>: lsr    w9, w12, w17
    0x197b051c4 <+132>: and    x9, x9, x11, lsr #53

    // x17 == sel_offs | (imp_offs << 32)
    0x197b051c8 <+136>: ldr    x17, [x10, x9, lsl #3]
    0x197b051cc <+140>: cmp    x12, w17, uxtw

    0x197b051d0 <+144>: b.ne   0x197b051dc               ; <+156>
    0x197b051d4 <+148>: sub    x17, x16, x17, lsr #32

    0x197b051d8 <+152>: br     x17

        /*** Start
        5.
        ***/
        // offset -8 is the fallback offse
        0x197b051dc <+156>: ldursw x9, [x10, #-0x8]
        // compute the fallback isa
        0x197b051e0 <+160>: add    x16, x16, x9
        // lookup again with a new isa
        0x197b051e4 <+164>: b      0x197b05154               ; <+20>
        /***************************/

    /***************************/


    /*** Start
    接收者为空时调用 LNilOrTagged
    ***/
    0x197b051e8 <+168>: b.eq   0x197b0520c               ; <+204>

        /*** Start
        GetTaggedClass
        ***/
        0x197b051ec <+172>: and    x10, x0, #0x7
        0x197b051f0 <+176>: asr    x11, x0, #55
        0x197b051f4 <+180>: cmp    x10, #0x7                 ; =0x7 
        0x197b051f8 <+184>: csel   x12, x11, x10, eq
        0x197b051fc <+188>: adrp   x10, 255422
        0x197b05200 <+192>: add    x10, x10, #0xb60          ; =0xb60 
        0x197b05204 <+196>: ldr    x16, [x10, x12, lsl #3]
        /***************************/

    0x197b05208 <+200>: b      0x197b05150               ; <+16> // LGetIsaDone
    /***************************/

    /*** Start
    LReturnZero
    ***/
    0x197b0520c <+204>: mov    x1, #0x0
    0x197b05210 <+208>: movi   d0, #0000000000000000
    0x197b05214 <+212>: movi   d1, #0000000000000000
    0x197b05218 <+216>: movi   d2, #0000000000000000
    0x197b0521c <+220>: movi   d3, #0000000000000000
    0x197b05220 <+224>: ret    
    /***************************/   

```

### 🎉 感谢能耐心看到这里，不错不错

* 看汇编是真的费劲儿

#### objc\_msgSend 汇编流程图

![objc\_msgSend汇编流程图.png](/files/-MdoBaNbF3y-qnL1qT2Z)

### 五、未命中缓存的处理 \_\_objc\_msgSend\_uncached

```C++
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class

// r10 is already the class to search
MethodTableLookup NORMAL	// r11 = IMP
jmp	*%r11			// goto *imp

END_ENTRY __objc_msgSend_uncached
```

这里会调用 `MethodTableLookup`。

#### 5.1 查找方法列表 MethodTableLookup

```C++
.macro MethodTableLookup
	// 创建一个栈，为调用方法做准备，存储寄存器中的值
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3

	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

    // 复原 SAVE_REGS用到的寄存器，出栈
	RESTORE_REGS MSGSEND

.endmacro
```

#### 5.2 慢速查找 \_lookUpImpOrForward

|       参数       |       说明      |
| :------------: | :-----------: |
|    `id inst`   |  receiver(x0) |
|    `SEL sel`   |  selector(x1) |
|   `Class cls`  |    ClassISA   |
| `int behavior` | method lookup |

> behavior: `LOOKUP_INITIALIZE | LOOKUP_RESOLVER)`

**流程图**

![慢速查找](/files/-MdoBaNc_e_FYptO4IfB)

**源码解析**

```C++
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    /**
    发给Class的第一个消息通常是通过了objc_opt_*或各种优化的入口：
    + new
    + alloc
    + self
    但是，此时Class尚未初始化
    */
    if (slowpath(!cls->isInitialized())) { 
        behavior |= LOOKUP_NOCACHE;
     }

    /** 
    加锁
    保证 method-lookup + cache-fill 的原子性。
    否则
    可能分类添加后会被永久忽略，因为缓存被旧的数据覆盖
    */
    runtimeLock.lock();

    /**
    为防止 CFI攻击
    检查是否是已注册的类
    确保Class编译进了二进制文件
    或者是使用 
    objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair
    合法的注册过的
    */
    checkIsKnownClass(cls);

    // 如果需要的话初始化class
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    /**
    死循环。通过goto和break跳出
    */
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            ...
        } else {
            // curClass method list.
            // 用二分法查找sel
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            // 这里将获取了父类并赋值给 curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // 父类都找完了（superClass == nil）也没有就进行消息转发了
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }
        ...

        // Superclass cache. 这里的 curClass 已经是父类了
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 需要进行消息转发
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    ...

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
    // iOS真机
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        // 插入缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
...
}

```

#### 5.3 二分查找赏析

这里的算法比较精妙，值得学习。

```C++
/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    /**
    count >>= 1，右移一位，即二分一次
    */
    for (count = list->count; count != 0; count >>= 1) {
        // 居中的下标
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            /**
            probe > first
            开始遍历一直把同名的遍历完，因为有分类的存在，所以可能会有同名的。
            会掉分类的。
            */
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
```

### 六、动态决议与消息转发

[动态决议与消息转发](/wiki/ios/di-ceng/13.-dong-tai-jue-yi-yu-xiao-xi-zhuan-fa.md)

### 附：知识点补充

#### 1. 取 buckets 过程的补充说明

```C++
// 判断 cache_t(p11) 第0位是否不为0，如果为0，是否有共享缓存（first_shared_cache_sel）
// 没缓存：继续走
// 有缓存：跳转 LLookupPreopt\Function
tbnz	p11, #0, LLookupPreopt\Function
// cache_t(p11) & #0x0000ffffffffffff -> p10(buckets)
// 取低 48 位
and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
```

* 这里的第0位，是指低位的0位

#### 2. **OBJC2**

我们在进行源码学习的时候时常会见到`__OBJC2__`这个宏定义。

* 因为`runtime`有两个版本
  * 1.0：用于早期系统上，32位的MacOS上
  * 2.0（现行版本）：`iOS`和`MacOS X 10.5`以后的64位系统上
* 想要了解更多可以查看下面的官方文档：
  * [Objective-C Runtime Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008048)
  * [Objective-C Runtime](https://developer.apple.com/documentation/objectivec/objective-c_runtime)

#### 3. cache\_t

[【Lawliet玩iOS源码】Class: cache\_t](/wiki/ios/di-ceng/10.class-cache_t.md)

#### 4. Class的实现（Realize）和初始化（Initialize）

[类的实现与初始化](/wiki/ios/di-ceng/12.-lei-de-shi-xian-yu-chu-shi-hua.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/ios/di-ceng/11.runtime-objc_msgsend.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
