iOS底层-Runtime及objc_msgSend快速查找

前言

在分析 cache_t原理 时,提及很多次的 objc_msgSend 函数,以及在真机环境下,cache缓存中多了个 maskZeroBits 字段,只知道 objc_msgSend 使用的,确不知道如何使用。

下面将探索 objc_msgSendruntime 的原理。

Runtime

Runtime 简介

  • 编译时 (building):顾名思义就是 正在编译 的时候,编译器把源代码翻译成机器能识别的代码,实际上只是翻译成某个中间状态的语⾔。最重要的是进行 词法分析语法分析,并且检查代码 是否符合规范,这个过程就叫 编译时类型检查,也叫 静态类型检查。这个过程代码没有加载到内存中,⽽只是把代码当作⽂本来扫描下。

  • 运行时 (running):代码跑起来,装载到内存 中。运行时检查错误和编译时检查错误 (或者静态类型检查) 不一样,不是简单的代码扫描,而是在内存中做操作和判断。

Runtime 提供了一套公共接口(函数和数据结构)的动态分享库,基本是用 CC++汇编 写的,可见苹果为了动态系统的高效而作出的努力。平时的业务中主要是使用官方Api,解决我们框架性的需求。

Runtime 版本

  • Legacy 版本:早期版本,对应的编程接口:Objective-C 1.032 位的 Mac OS X 的平台

  • Modern 版本:现行版本,对应的编程接口:Objective-C 2.0iPhoneMac OS X 10.5 及以后的系统中的 64 位系统。

Runtime 调用方式

Runtime 调用方式有三种:

Objective-C 代码方式

需要编写和编译 Objective-C 源码,runtime 将完成 OC 编译成 C 的过渡,实现底层 runtime API 的转变,比如:

编译成底层源码:

方法的调用其实是底层在调用 objc_msgSend 函数,下面会详细分析其原理。

Framework & Serivce 等系统API

调用 isKindOfClass 的方法分析其原理:

编译成底层源码:

  • NSObject 的方法底层也是在调用 objc_msgSend 函数。sel_registerName 获取对应的 SELobjc_getClass 获取实例对象。

  • 其实不只是 isKindOfClass: 方法,respondsToSelector: 是否接收特定消息;conformsToProtocol: 是否声明实现协议方法;methodForSelector: 方法地址的获取。

Runtime API

直接调用 runtime API 的方式

  • objc_msgSend 函数需要导入 <objc/message.h>
  • 需要修改 objc_msgSend 配置,Enable Strict Checking of objc_msgSend Calls 设置为 NO,默认值为 YES,意思是编译器会检查,objc_msgSend 只接收1个参数。此处目的不让编译器检查。

打印结果:

许多 Objective-C 代码可以通过 runtime API 替换为C 实现。

方法的本质

objc_msgSend

方法的调用就是消息发送,调用底层 objc_msgSend 函数,例如:

消息发送:objc_msgSend(消息接收者,消息的主体(sel + 参数))

objc_msgSendSuper

添加一个 ZLObject 的子类 ZLSubObject ,并重写doSomething,如下:

编译成源码:

  • 父类消息发送:objc_msgSendSuper(父类接收者,消息主体(sel + 参数))

  • 子类对象可以通过 objc_msgSendSuper 方式调用父类的方法。

objc_msgSendSuper 定义如下:

如果想要模拟调用 objc_msgSendSuper 函数,就必须了解其方法参数意义。最主要的还是 super 结构体,结构如下:

当前版本是 __OBJC2__ ,最终简化为:

调用 objc_msgSendSuper 函数:

打印结果:

方法的本质:消息接收者通过 sel 查找 imp 的过程。

objc_msgSend 快速查找

objc_msgSend 汇编源码:

// objc_msgSend 汇编入口 (主要是通过 isa 拿到接收者类,进而拿到cache)
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    // cmp: 比较指令(其本质是做减法), 不影响寄存器的值.
    // 此处判断接收者是否存在, p0是接收者, objc_msgSend的第一个参数: (id)receiver
    cmp p0, #0
#if SUPPORT_TAGGED_POINTERS (__LP64__架构执行)
    // le: 小于等于0 (p0不存在)
    b.le    LNilOrTagged
#else (!__LP64__架构执行)
    // eq: 等于0, 接收者不存在。
    b.eq    LReturnZero
#endif
    // ldr: 取值指令, x0是receiver,拿出isa,即从x0寄存器指向的地址取出isa,存入p13寄存器
    // p13 = isa
    ldr p13, [x0]
    // 通过 p13(isa) & ISA_MASK,得到class信息。
    // p16 = class
    GetClassFromIsa_p16 p13, 1, x0
LGetIsaDone:
    // 如果有isa,走到 CacheLookup 即缓存查找流程,也就是sel->imp 快速查找流程
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS (__LP64__架构执行)
LNilOrTagged:
    // 清空寄存器,跳出 _objc_msgSend函数
    b.eq    LReturnZero
    // 通过tagged pointer 获取class
    GetTaggedClass
    // 进入缓存查找流程
    b   LGetIsaDone
#endif

LReturnZero:
    // x0寄存器存的是0 (接收者不存在)
    // 清空数据
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    // 跳出 _objc_msgSend函数
    ret

    END_ENTRY _objc_msgSend

1. 核心逻辑:

  • 先判断 receiver 是否 <=0

  • 如果是 <= 0,再判断架构,如果是 __LP64__ 则通过 GetTaggedClass 获取 class,进入 CacheLookup 查找流程;否则直接清空寄存器退出。

  • 如果 receiver 存在,通过 GetClassFromIsa_p16 获取 class,进入 CacheLookup 查找流程。

2. 伪代码:

void objc_msgSend(receiver, _cmd) {
    if (receiver > 0) {
        // 获取cls
        cls = GetClassFromIsa_p16(isa,1,isa)
    } else {
        if (__LP64__ && receiver <= 0) {
            // 获取cls
            cls = GetTaggedClass();
        } else {
            return;
        }
    }
    // 快速查找
    CacheLookup(NORMAL, _objc_msgSend,  __objc_msgSend_uncached)
}

GetTaggedClass 汇编源码:

.macro GetTaggedClass
    // target pointer 的占位分布为: 1(标记位) + 8(extended) + 52(data) + 3(tag) = 64位
    // x10 = isa & 111 : 获取后3位tag
    and x10, x0, #0x7
    // x11 = isa >> 55 : 获取前9位(1(标记位) + 8(extended))
    asr x11, x0, #55
    // 判断x10(标记位) 是否等于7
    cmp x10, #7
    // 三目运算
    // 如果 (x10 == 7) ? x12 = x11 = 1(标记位) + 8(extended)
    // 否则 x12 = x10 = 3(tag)
    csel    x12, x11, x10, eq
    // x16 = _objc_debug_taggedpointer_classes[x12]
    // 将_objc_debug_taggedpointer_classes的基址 读入x10寄存器
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    // x10 = x10 + _objc_debug_taggedpointer_classes的基址偏移量(内存偏移, 获取 classes 数组)
    add    x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    // x16 = x10 + x12 << 3 = class
    ldr x16, [x10, x12, LSL #3]
.endmacro

1. 核心逻辑:

  • 获取低 3tag 和 高 9 位 (标志位+extended位) 。

  • 获取 taggedpointer 内存平移量

  • 通过平移量偏移,获取 class

2. 伪代码:

Class GetTaggedClass() {
    x10 = isa & 111;
    x11 = isa >> 55;
    x12 = x10 == 7 ? x11 : x10;
    x10 = x10 << (x12 & @PAGE);
    x10 = x10 & @PAGEOFF;
    // 通过偏移找到cls
    cls = x16 = x10 + x12 << 3;
    return cls;
}

GetClassFromIsa_p16 汇编源码:

.macro GetClassFromIsa_p16 src, needs_auth, auth_address
// armv7k or arm64_32
#if SUPPORT_INDEXED_ISA
    // 将isa的值存入p16寄存器
    mov p16, \src
    // tbz: 标记位为0则跳转。
    // 判断 p16 的第0位为0,如果成立则跳转1f。(ISA_INDEX_IS_NPI_BIT = 0)
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f
    // 将_objc_indexed_classes的基址 读入x10寄存器
    adrp    x10, _objc_indexed_classes@PAGE
    // x10 = x10 + _objc_indexed_classes的基址偏移量(内存偏移, 获取 classes 数组)
    add x10, x10, _objc_indexed_classes@PAGEOFF
    // ubfx: 无符号位域提取指令
    // 从p16第2位开始提取,提取15位。目的提取的indexcls, 剩余位补0。
    // (armv7k or arm64_32下,ISA_INDEX_SHIFT = 2 和 ISA_INDEX_BITS = 15)
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS
    // p16 = x10[indexcls & PTRSHIFT] (PTRSHIFT在 __LP64__ 下是3, 其余是2)
    // p16 = class
    ldr p16, [x10, p16, UXTP #PTRSHIFT]
1: // tbz 成立跳转

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp 已经授权了类
    // 无需授权将isa的值存入p16寄存器
    mov p16, \src
.else
    // p16 = isa & ISA_MASK = class
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 将isa的值存入p16寄存器
    mov p16, \src
#endif
.endmacro

1. 核心逻辑如下:

GetClassFromIsa_p16 的目的就是获取 class 给到 p16

  • armv7k or arm64_32

    判断标记位是否为 0 (是否开启了 nonpointer),如果开启,根据偏移量找到 indexcls,再通过 操作,得到 cls 并赋值给 p16;否则,直接取 cls 赋值给 p16

  • __LP64__

    判断是否需要授权(needs_auth),如果需要,调用 ExtractISAisa & ISA_MASK 结果赋值给 p16;否则将 isa 的值存入 p16 寄存器。( _objc_msgSendneeds_auth 为1 )

2. 伪代码:

Class GetClassFromIsa_p16 (isa, needs_auth, auth_address) {
    Class cls = NULL;
    if (arm64_32 || armv7k) {
        if (nonpointer) {
            class[] = x10 << (12 & @PAGE);
            indexcls = ubfx isa[2-15]
            cls = class[indexcls]
        } else {
            cls = isa;
        }
    } else if (__LP64__) {
        if (needs_auth) {
            cls = ExtractISA();
        } else {
            cls = isa;
        }
    } else {
        cls = isa;
    }
    return cls;
}

ExtractISA 汇编源码:

// ISA signing authentication modes. Set ISA_SIGNING_AUTH_MODE to one
// of these to choose how ISAs are authenticated.
#define ISA_SIGNING_STRIP 1 // Strip the signature whenever reading an ISA.
#define ISA_SIGNING_AUTH  2 // Authenticate the signature on all ISAs.

// ISA signing modes. Set ISA_SIGNING_SIGN_MODE to one of these to
// choose how ISAs are signed.
#define ISA_SIGNING_SIGN_NONE       1 // Sign no ISAs.
#define ISA_SIGNING_SIGN_ONLY_SWIFT 2 // Only sign ISAs of Swift objects.
#define ISA_SIGNING_SIGN_ALL        3 // Sign all ISAs.

#if __has_feature(ptrauth_objc_isa_strips) || __has_feature(ptrauth_objc_isa_signs) || __has_feature(ptrauth_objc_isa_authenticates)
#   if __has_feature(ptrauth_objc_isa_authenticates)
#       define ISA_SIGNING_AUTH_MODE ISA_SIGNING_AUTH
#   else
#       define ISA_SIGNING_AUTH_MODE ISA_SIGNING_STRIP
#   endif
#   if __has_feature(ptrauth_objc_isa_signs)
#       define ISA_SIGNING_SIGN_MODE ISA_SIGNING_SIGN_ALL
#   else
#       define ISA_SIGNING_SIGN_MODE ISA_SIGNING_SIGN_NONE
#   endif
#else
#   if __has_feature(ptrauth_objc_isa)
#       define ISA_SIGNING_AUTH_MODE ISA_SIGNING_AUTH
#       define ISA_SIGNING_SIGN_MODE ISA_SIGNING_SIGN_ALL
#   else
#       define ISA_SIGNING_AUTH_MODE ISA_SIGNING_STRIP
#       define ISA_SIGNING_SIGN_MODE ISA_SIGNING_SIGN_NONE
#   endif
#endif

// A12以后机型
#if __has_feature(ptrauth_calls)
.macro ExtractISA
    // $0 = $1 & ISA_MASK
    and $0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
    // 每次读isa时都授权 (当前架构会执行)
    xpacd   $0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    // 授权所有的isa (不会执行)
    mov x10, $2
    // movk: 位移指令,movk移动高x位的到寄存器,其余空位保持原样;
    // x10 = #0x6AE1 << 48 (ISA_SIGNING_DISCRIMINATOR = 0x6AE1)
    movk    x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
    // autda: 授权向后读取x10给p16
    autda   $0, x10
#endif
.endmacro
#else
.macro ExtractISA
    // $0 = $1 & ISA_MASK
    and    $0, $1, #ISA_MASK
.endmacro
#endif

核心逻辑如下:

  • ExtractISA 的目的通过 isa & ISA_MASK 获取 class

  • A12 机型做了认证逻辑的判断,其中 xpacd 指令不是很熟悉。

CacheLookup 汇编源码(重点):

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

    // p16存储的是cls,x15 = [#x16] = cls (将cls复制一份存到x15)
    mov x15, x16
// 开始查找
LLookupStart\Function:
    // p1 = SEL, p16 = isa
// arm64 64位 OSX | SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    // 其中 (CACHE = 2 * __SIZEOF_POINTER__ = 2 * 8 = 16位,__SIZEOF_POINTER__是指针,占8位)
    // p10 = isa & #CACHE = _bucketsAndMaybeMask(cache_t首地址)
    ldr p10, [x16, #CACHE]
    // lsr: 逻辑右移
    // p11 = p10(_bucketsAndMaybeMask) >> 48 = mask。(获取mask)
    lsr p11, p10, #48
    // p10 = p10(_bucketsAndMaybeMask) & 0xffffffffffff(12位) = buckets。(只保留48位,获取buckets)
    and p10, p10, #0xffffffffffff
    // w1: 第二个参数SEL,w11 = p11 = mask ( 执行cache_hash()函数,获取插入begin )
    // p12 = SEL & mask = 插入begin
    and w12, w1, w11
// arm64 64位 真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // p11 = isa & #CACHE = _bucketsAndMaybeMask(cache_t首地址)
    ldr p11, [x16, #CACHE]
// arm64 IOS && !模拟器 && !mac应用
#if CONFIG_USE_PREOPT_CACHES
// A12真机
#if __has_feature(ptrauth_calls)
    // tbnz: 标记位不为0则跳转(与tbz相反)。
    // p11第0位不为0则跳转 LLookupPreopt
    tbnz    p11, #0, LLookupPreopt\Function
    // p10 = p11(_bucketsAndMaybeMask) & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff
// 其他真机
#else
    // p10 = p11(_bucketsAndMaybeMask) & 0x0000fffffffffffe = buckets
    and p10, p11, #0x0000fffffffffffe
    // p11第0位不为0则跳转 LLookupPreopt
    tbnz    p11, #0, LLookupPreopt\Function
#endif // __has_feature(ptrauth_calls)
    // eor: 异或指令
    // p12 = sel ^ (sel >> 7) ( 执行cache_hash()函数,真机执行 >>7操作 )
    eor p12, p1, p1, LSR #7
    // p11(_bucketsAndMaybeMask) >> 48 = mask
    // p12 = (sel ^ (sel >> 7)) & mask
    and p12, p12, p11, LSR #48
#else
    // p10 = p11(_bucketsAndMaybeMask) & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff
    // p11(_bucketsAndMaybeMask) >> 48 = mask
    // p12 = sel & mask
    and p12, p1, p11, LSR #48
#endif // CONFIG_USE_PREOPT_CACHES
// 非64位
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // p11 = isa & #CACHE = _bucketsAndMaybeMask(cache_t首地址)
    ldr p11, [x16, #CACHE]
    // p10 = p11 & ~0xf = buckets(前28位), 相当于后4位置为0,取前28位
    and p10, p11, #~0xf
    // p11 = p11 & 0xf = maskShift(后4位), 相当于前28位置为0,取后4位
    and p11, p11, #0xf
    // p12 = 0xffff
    mov p12, #0xffff
    // p11 = p12 >> p11 = 0xffff >> maskShift = mask
    lsr p11, p12, p11
    // p12 = sel & mask
    and p12, p1, p11
#else
#error Unsupported cache mask storage for ARM64.
#endif
    
    // p10 = buckets; p12 = sel & mask; 64位下PTRSHIFT = 3, 其他PTRSHIFT = 2
    // p13 = buckets & ((sel & mask) << (1 + PTRSHIFT)) = bucket
    add p13, p10, p12, LSL #(1+PTRSHIFT)
    // {p17(imp), p9(sel)} = *p13[BUCKET_SIZE--] (内存平移)
1:  ldp p17, p9, [x13], #-BUCKET_SIZE
    // 比较得到的p9 == p1 ?
    cmp p9, p1
    // 如果不相等(没找到), 跳转第3步
    b.ne    3f

    // 如果命中,执行 CacheHit
2:  CacheHit \Mode
    
    // cbz: 为0跳转。
    // 判断得到的sel == 0,如果成立,跳转MissLabelDynamic
3:  cbz p9, \MissLabelDynamic
    // 判断 bucket >= buckets
    cmp p13, p10
    // 如果大于等于(bucket >= buckets 成立), 跳转第1步
    b.hs    1b

// 如果都没有命中 bucket, 查找 p13 = mask对应的元素
// p10 = buckets; p11 = mask;
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    // UXTW: 扩展指令, 将w11左移1+PTRSHIFT后扩展为64位
    // p13 = buckets & (mask << 1+PTRSHIFT) = bucket
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
                
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 此处忽略了maskZeroBits的位数
    // p13 = buckets & (mask >> (48 - (1+PTRSHIFT)) = bucket
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // p13 = buckets & (mask << 1+PTRSHIFT) = bucket
    add p13, p10, p11, LSL #(1+PTRSHIFT)

#else
#error Unsupported cache mask storage for ARM64.
#endif
    // p12 = sel & mask
    // p12 = buckets & (sel & mask << (1+PTRSHIFT)) = first_probed bucket
    add p12, p10, p12, LSL #(1+PTRSHIFT)

    // {p17(imp), p9(sel)} = *p13[BUCKET_SIZE--] (内存平移)
4:  ldp p17, p9, [x13], #-BUCKET_SIZE
    // 判断 p9(sel) = _cmd
    cmp p9, p1
    // 如果相等,跳转第2步 CacheHit
    b.eq    2b
    // 如果不相等,判断p9(sel) 是否等于0
    cmp p9, #0
    // 并且 p13(bucket) > p12(first_probed bucket)
    ccmp    p13, p12, #0, ne
    // hi: 无符号大于,再次执行第4步
    b.hi    4b

LLookupEnd\Function:
LLookupRecover\Function:
    b   \MissLabelDynamic

1. 核心逻辑如下:

  • 总体流程:CacheLookupbucketsbucketsel & imp

  • 判断架构,并找到 buckets,以及 bucketssel 对应的 indexp10 = buckets,p11 = mask,p12 = index

  • arm64 64位 的情况下如果 _bucketsAndMaybeMask0 位不为 0 则执行 LLookupPreopt()

  • 两个循环查找过程:

    • 首先 buckets & index << 4 找到对应的 bucketdo-while 循环判断 buckets[index],如果能找到 sel,说明命中,直接执行 CacheHit;如果存在sel为空,则说明是没有缓存的,就直接 __objc_msgSend_uncached()

    • 平移获得 p13 = buckets[mask] 对应的元素,也就是倒数第二个。p13 = buckets & mask << 4 找到 mask 对应的 bucketdo-while 循环判断 buckets[mask]sel,直到 index。如果命中,执行 CacheHit;如果存在sel为空,则说明是没有缓存的,就结束循环。

  • 最终仍然没有找到则执行 __objc_msgSend_uncached()

  • 真机环境下,maskZeroBits 始终是4位0 (0x0000ffffffffffff | 0x0000fffffffffffe),获取 bucket 时,_bucketsAndMaybeMask >> 44 后就不用再 <<4 找到对应 bucket 的地址。

  • f b 分别代表 frontback,跳转顺序往前( f )往后( b )的意思。

2. 伪代码:

// CacheLookup(NORMAL, _objc_msgSend, __objc_msgSend_uncached, NULL)
void CacheLookup(Mode, Function, MissLabelDynamic, MissLabelConstant) {
    x15 = x16;
    if (__LP64__ && (TARGET_OS_OSX || TARGET_OS_SIMULATOR)) {
        _bucketsAndMaybeMask = isa & 16;
        mask = _bucketsAndMaybeMask >> 48;
        buckets = _bucketsAndMaybeMask & 0xffffffffffff;
        index = sel & mask;
    } else if (__LP64__) {
        // _bucketsAndMaybeMask
        _bucketsAndMaybeMask = isa & 16;
        if (IOS && !SIMULATOR && !MACCATALYST) {
            // A12
            if (__has_feature(ptrauth_calls)) {
                // 判断标记位
                if (_bucketsAndMaybeMask[第0位] != 0) {
                    LLookupPreopt();
                } else {
                    buckets = _bucketsAndMaybeMask & 0x0000ffffffffffff
                }
            } else {
                buckets = _bucketsAndMaybeMask & 0x0000fffffffffffe;
                // 判断标记位
                if (_bucketsAndMaybeMask[第0位] != 0) {
                    LLookupPreopt();
                }
            }
            // 计算index
            mask = _bucketsAndMaybeMask >> 48;
            index = sel ^ (sel >> 7) & mask;
        } else {
            buckets = _bucketsAndMaybeMask & 0x0000ffffffffffff;
            mask = _bucketsAndMaybeMask >> 48;
            // 计算index
            index = sel & mask;
        }
    } else if (!__LP64__) {
        _bucketsAndMaybeMask = isa & 16;
        buckets = _bucketsAndMaybeMask & ~0xf;
        maskShift = _bucketsAndMaybeMask & 0xf;
        mask = 0xffff >> maskShift;
        index = sel & mask;
    }
    // 获取目标
    bucket = buckets & (index << 4);
    i = index;
    // 循环
    do {
        bucket = buckets[i--];
        if (bucket.sel() == _cmd) {
            // 命中
            CacheHit(NORMAL);
            return;
        }
        if (bucket.sel() == 0) {
            // __objc_msgSend_uncached()
            MissLabelDynamic();
            return;
        }
    } while(bucket >= buckets);
    
    // 如果没找到缓存
    if (__LP64__ && (TARGET_OS_OSX || TARGET_OS_SIMULATOR)) {
        bucket = buckets & (mask << 4);
    } else if (__LP64__) {
        bucket = buckets & (mask >> 44);
    } else if (!__LP64__) {
        bucket = buckets & (mask << 3);
    }
    index = sel & mask;
    first_probed = buckets & (index << 4);
    i = index;
    // 循环
    do {
        bucket = buckets[i--];
        if (bucket.sel() == _cmd) {
            // 命中
            CacheHit(NORMAL);
            return;
        }
    } while(bucket.sel() != 0 && bucket > first_probed);
    // 仍然没有找到缓存,缓存彻底不存在。
    __objc_msgSend_uncached()
}

LLookupPreopt 汇编源码:

LLookupPreopt\Function:
// A12机型
#if __has_feature(ptrauth_calls)
    // p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets
    and p10, p11, #0x007ffffffffffffe
    // 授权向前读取 x10 = cls
    autdb   x10, x16
#endif
    // p9 = _cmd
    adrp    x9, _MagicSelRef@PAGE
    // _cmd = (_cmd >> 12 + @PAGE) << 12 + PAGEOFF
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    // p12 = _cmd - p9 = index
    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
    // p17 = _bucketsAndMaybeMask >> 55 = hash_shift
    lsr x17, x11, #55
    // p9 = index >> hash_shift
    lsr w9, w12, w17
    // p17 = _bucketsAndMaybeMask >> 60 = mask_bits = 4
    lsr x17, x11, #60
    // p11 = #0x7fff
    mov x11, #0x7fff
    // p11 = #0x7fff >> mask_bits = mask
    lsr x11, x11, x17
    // p9 = p9 & mask
    and x9, x9, x11
#else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    // p17 = _bucketsAndMaybeMask >> 48 = hash_shift
    lsr x17, x11, #48           // w17 = (hash_shift, hash_mask)
    // p9 = index >> hash_shift
    lsr w9, w12, w17
    // p11(_bucketsAndMaybeMask) >> 53 = mask
    // p9 = p9 & mask
    and x9, x9, x11, LSR #53
#endif
    
    // x17 == sel_offs | (imp_offs << 32)
    ldr x17, [x10, x9, LSL #3]
    // 比较index 和 p17
    cmp x12, w17, uxtw

.if \Mode == GETIMP
    // 如果不等于, cache miss
    b.ne    \MissLabelConstant
    // p0 = isa - p17 >> 32 = imp
    sub x0, x16, x17, LSR #32
    // 寄存imp到p0,并返回
    SignAsImp x0
    ret
.else
    // 如果不等于, 跳转5
    b.ne    5f
    // p17 = isa - p17 >> 32 = imp
    sub x17, x16, x17, LSR #32
.if \Mode == NORMAL
    // 跳转p17寄存器
    br  x17
.elseif \Mode == LOOKUP
    // p16 = p16 | 0x11
    orr x16, x16, #3
    // 寄存imp到p17,并返回
    SignAsImp x17
    ret
.else
.abort  unhandled mode \Mode
.endif

    // p9 = *buckets--
5:  ldursw  x9, [x10, #-8]
    // x16 = x16 & x9
    add x16, x16, x9
    // 跳转 LLookupStart
    b   LLookupStart\Function
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

1. 核心逻辑如下:

  • LLookupPreopt 目的就是获取 imp 并返回。

  • 如果找不到,再次进行缓存查找。

2. 伪代码:

void LLookupPreopt() {
    if (__has_feature(ptrauth_calls)) {
        buckets = _bucketsAndMaybeMask & 0x007ffffffffffffe;
    }
    // 计算差值index
    index = (_cmd - first_shared_cache_sel);
    if (__has_feature(ptrauth_calls)) {
        hash_shift = _bucketsAndMaybeMask >> 55;
        mask_bits = _bucketsAndMaybeMask >> 60;
        mask = 0x7fff >> mask_bits;
        _cmd = (index >> hash_shift) && mask;
    } else {
        hash_shift = _bucketsAndMaybeMask >> 48;
        mask = _bucketsAndMaybeMask >> 53;
        _cmd = (index >> hash_shift) && mask;
    }
    // 判断模式
    if (Mode == GETIMP) {
        if (index == sel_offs) {
            imp = isa - p17 >> 32;
            return imp;
        } else {
            return 0;
        }
    } else {
        if (index == sel_offs) {
            imp = isa - p17 >> 32;
            if (Mode == NORMAL) {
                return imp;
            } else if (Mode == LOOKUP) {
                isa = isa | 0x11;
                return imp;
            }
        } else {
            isa = isa & *buckets--;
            LLookupStart();
        }
    }
}

CacheHit 汇编源码:

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// A12以后机型
#if __has_feature(ptrauth_calls)
.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // $1 = buckets ^ SEL;
    eor    $1, $1, $2    // mix SEL into ptrauth modifier
    // $1 = (buckets ^ SEL) ^ isa;
    eor    $1, $1, $3  // mix isa into ptrauth modifier
    // $0(p17) = (buckets ^ SEL) ^ isa;
    brab    $0, $1
.endmacro

.macro AuthAndResignAsIMP
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // $1 = buckets ^ SEL;
    eor    $1, $1, $2    // mix SEL into ptrauth modifier
    // $1 = (buckets ^ SEL) ^ isa;
    eor    $1, $1, $3  // mix isa into ptrauth modifier
    // 授权向前存储到$0中 $0 = $1
    autib    $0, $1
    // xar: 零寄存器
    // 读取$0到零寄存器,如果读取成功,说明是验证失败的
    ldr    xzr, [$0]
    // paciza: 对地址进行 paciza 签名
    paciza    $0
.endmacro
#else
.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // $0 = imp ^ isa
    eor    $0, $0, $3
    br    $0
.endmacro

.macro AuthAndResignAsIMP
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
    // $0 = imp ^ isa
    eor    $0, $0, $3
.endmacro
#endif

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    // 验证并记录imp
    // TailCallCachedImp(imp, buckets, sel, isa)
    TailCallCachedImp x17, x10, x1, x16
.elseif $0 == GETIMP
    mov p0, p17
    // 比较p0是不是等于0, 如果是0, 跳转到9
    cbz p0, 9f
    // 如果不为0,验证签名为imp
    AuthAndResignAsIMP x0, x10, x1, x16
// return IMP
9:  ret
.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.
    // 授权签名imp
    AuthAndResignAsIMP x17, x10, x1, x16
    // 比较p16和p15, (p15是开始调用_objc_msgSend时,p15 = p16赋值的)
    cmp x16, x15
    // 当p15 != p16,时p16++;
    cinc    x16, x16, ne
    //  return imp
    ret
.else
.abort oops
.endif
.endmacro

1. 核心逻辑如下:

  • _objc_msgSend 会执行 NORMAL 逻辑,会直接执行 TailCallCachedImp,其内部执行 (buckets ^ SEL) ^ isa,对 imp 进行了解码,并返回 imp

  • GETIMPLOOKUP 都会授权注册imp,并返回 imp。不同的是 GETIMP 是判断之后再授权 impLOOKUP 先授权 imp 再判断。

2. 伪代码:

IMP CacheHit(mode) {
    if (mode == NORMAL) {
        return TailCallCachedImp(imp, buckets, sel, isa);
    } else if (mode == GETIMP) {
        if (imp == 0) {
            return imp;
        } else {
            return AuthAndResignAsIMP(imp, buckets, sel, isa);
        }
    } else if (mode == LOOKUP) {
        imp = AuthAndResignAsIMP(imp, buckets, sel, isa);
        if (curCls != firstCls) {
            curCls++;
        }
        return imp;
    } else {
        return 0;
    }
}

IMP TailCallCachedImp(imp, buckets, sel, isa) {
    if (__has_feature(ptrauth_calls)) {
        imp = buckets ^ sel;
        imp = imp ^ isa;
        return imp;
    } else {
        imp = imp ^ isa;
        return imp;
    }
}

IMP AuthAndResignAsIMP(imp, buckets, sel, isa) {
    if (__has_feature(ptrauth_calls)) {
        imp = buckets ^ sel;
        imp = imp ^ isa;
        if (imp == 0) {
            paciza imp;
        }
        return imp;
    } else {
        imp = imp ^ isa;
        return imp;
    }
}

__objc_msgSend_uncached

在没有命中缓存时,会执行 __objc_msgSend_uncached 函数。

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
// 调用 MethodTableLookup (涉及到慢速查找逻辑)
MethodTableLookup
// 返回 imp
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached

主要执行 MethodTableLookup,其中涉及到了慢速查找。下篇进行分析。

objc_msgSend快速查找流程图

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容