美文网首页
iOS底层-Runtime及objc_msgSend快速查找

iOS底层-Runtime及objc_msgSend快速查找

作者: 忻凯同学 | 来源:发表于2021-07-02 15:32 被阅读0次

    前言

    在分析 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快速查找流程图

    相关文章

      网友评论

          本文标题:iOS底层-Runtime及objc_msgSend快速查找

          本文链接:https://www.haomeiwen.com/subject/eqhhultx.html