objc_msgSend 一些小总结

2021-08-26  本文已影响0人  Priders

objc_msgSend

从编译后的CPP文件开始

        Man * msgMan = [[Man alloc]init];
        
xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
===>
        Man *msgMan = ((Man *(*)(id, SEL))(void *)objc_msgSend)((id)
        ((Man *(*)(id, SEL))(void *)objc_msgSend)
        ((id)objc_getClass("Man"), sel_registerName("alloc")), 
        sel_registerName("init"));

简化一下 摸索了一下
===> 其实就是俄罗斯套娃 哈哈 目前使用了 函数指针的形式调用 
    以前是使用objc_msgSend(id,SEL)这样 前提必须设置Enbale Strict Checking of objc_msgSend Calls为NO 
    如果为YES 我们必须使用如下解释的函数指针的方式 直接跳过检查
    Man *msgMan = ((Man *(*)(id, SEL))(void *)objc_msgSend)
    ((id)((Man *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Man"), sel_registerName("alloc")), //id
    sel_registerName("init"));//SEL
再简化一下
===>
    id classMan = ((Man *(*)(id, SEL))(void *)objc_msgSend) //id
    ((id)objc_getClass("Man"), sel_registerName("alloc")) //SEL
    Man *msgMan =  ((Man *(*)(id, SEL))(void *)objc_msgSend)
    (classMan,  //id
     sel_registerName("init")) //SEL
    
最细解释
===>
    void *msgSendIMP = (void *)objc_msgSend;
    Man *(*funcMsgSend)(id,SEL) = msgSendIMP;
    //有这里可知 原代码的地方 使用了匿名函数指针(Man *(*)(id, SEL)) 
    //一开始看懵逼到 恍然大悟 
    Man *msgMan = funcMsgSend((id)objc_getClass("Man"),sel_registerName("alloc"));
    由此递推 alloc init 等其他方法
    
    //看不懂看这里
    //看看C++的函数指针相关的内容
    //IMP 相关 
    执行IMP void (*func)(id, SEL, id) = (void *)imp;  func(self, methodName,param);
    注意分析:如果方法没有传入参数时:void (*func)(id, SEL) = (void *)imp;   func(self, methodName);
    如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp;   func(self, methodName,param);
    如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp;   func(self, methodName,param1,param2);

objc_msgSend

在开始objc_msgSend方法的学习时候,以及对平常使用的oc代码进行 消息发送的本质 底层方法是具体如何去理解,调用的进行总结。

现在开始真正对这个方法进行学习。
在objc/Source里可以看到 objc-msg-XXX.s(),说明这个方法和__dyld_start一样用汇编语言进行实现的。原因:提高消息发送的过程,使用汇编节省高级语言翻译过程,编译成机器码就可以了。
一开始我觉得看不懂汇编,看着里面的内容发现 汇编那么多个内容 你不懂我也不懂 那肯定要有注释 不然谁看懂 整挺好 源码内部真的有注释

objc-msg-simulator-x86_64.s
objc-msg-arm.s
objc-msg-arm64.s
objc-msg-i386.s
objc-msg-simulator-i386.s
objc-msg-x86_64.s

针对看objc-msg-arm64.s 相关的。
找到其入口_objc_msgSend 还有一些别后缀的 直接搜索定位

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    //第一个为信息接受者 判断是否为0 如果是直接返回LReturnZero
    cmp  p0, #0         // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa 拿第一个的地址 也就是isa 
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class 获取它的类 存在 p16里面
LGetIsaDone: //名如其目的 get完isa done 往下走
    // calls imp or objc_msgSend_uncached
    //查询缓存 模式?NORMAL Function当前调用的方法 MissLabelDynamic 估计是miss 没命中之后的动态调用?
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached 

接下来进入 CacheLookup 关于缓存查找

#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

去看新版的源码的时候 有点吓人 新旧变化那么多?
我删除一些暂为用到的 我考虑的是 arm64的

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    mov x15, x16            // stash the original isa
LLookupStart\Function:
    // p1 = SEL, p16 = isa
...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
#else
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets
    tbnz    p11, #0, LLookupPreopt\Function
#endif
    eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 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
...
#else
#error Unsupported cache mask storage for ARM64.
#endif

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

                        // do {
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel != _cmd) {
    b.ne    3f              //         scan more
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b

    // 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.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // p13 = buckets + (mask << 1+PTRSHIFT)
                        // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
...
#else
#error Unsupported cache mask storage for ARM64.
#endif
    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
//__has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能
//ptrauth_calls 指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备
//(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构

这一大段 emmm 太难受了 但是通过其他办法来理解 对比新旧代码 以及在cache_t里面的插入缓存方法
在旧版中 还存在 这个find方法 在新版中已经没有了 集合合在insert里了。

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

对比来学习 也就是说
在此查询的目的就是 在汇编的角度来拿出它的buckets 接着key值为:(mask_t)(value & mask); //value 相当sel 拿到之后进行遍历(mast转一圈又回到原处 来结束)这里查到到直接return 不插入 如果在查找过程 找到了 就应该调用 CacheHit \Mode // hit: call or return imp现在是insert过程 所以是return
找到了是0那就说明是空的插入set 但是在我们查找过程 为0就应该miss cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
再回去一字一字的去看 汇编 发现 简单如斯

接着没命中(这才能继续往下走)MissLabelDynamic 还记得当初传进来的是啥? __objc_msgSend_uncached!!! 对的 定位到这个地方(在Mac 上用commonLine 断点调试 也可以看到这个)

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 //这个是一个不可调用的?C函数?可能后期改为C?
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    
.macro MethodTableLookup
    
    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16 //应该给我们准备 cls 以及后面的传入参数
    mov x3, #3 
    bl  _lookUpImpOrForward //接着调用这个_lookUpImpOrForward

    // IMP in x0
    mov  x17, x0

    RESTORE_REGS MSGSEND

.endmacro

简单看一下上面的源码 其实最终就调用到了.
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
checkIsKnowClass // 防止不是class类的? CFI 控制流完整性(Control Flow Integrity, CFI)的防御机制
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
这就是调用我们的常用的initialize方法之处 盲猜 必须要标志位调一次而已 (cls->isInitialized).
接下来就进入到了 类和父类的方法列表查找了(旧版本很清晰 新版本 维护了一个for 无退出条件的循环 需要break)

  1. 先是判断有没有支持缓存优化 有的话就查询cache中缓存 cache_getImp(curClass, sel) 接着从curClass->cache.preoptFallbackClass再拿缓存。奇怪 如果这里一直拿不到 怎么出来到我的方法中拿呢?猜测是cache.preoptFallbackClass移动回来得到的不会进入这个缓存优化。
  2. Method meth = getMethodNoSuper_nolock(curClass, sel);获取目前class的方法列表 通过SEL查找对应的方法,查询到跳转done
  3. 在2中拿不到就 把curClass 设置为父类 接着查询父类的缓存,接着就是第二个循环里面 查询当前类(第一次的父类)的方法列表 继续循环下去
  4. 知道找到 缓存起来 log_and_fill_cache(cls, imp, sel, inst, curClass); 并且返回imp 或者 没找到break 跳出循环 走动态方法决议。
上一篇 下一篇

猜你喜欢

热点阅读