runtime和方法的本质

前言

前面我探究了cache_t方法的缓存机制,接下来我们理应探究方法的查找了,但是在探究方法查找之前我们需要明白rutime方法的本质

一、runtime

什么是runtime?简单的说就是运行时。还有一种编译时

  • 编译时就是简单的作⼀些翻译⼯作 ,⽐如检查⽼兄你有没有粗⼼写错啥关键字了啊.有啥词法分析,语法分析之类的过程. 就像个⽼师检查学⽣的作⽂中有没有错别字和病句⼀样 .如果发现啥错误编译器就告诉你.如果你⽤微软的VS的话,点下build.那就开始编译,如果下⾯有errors或者warning信息,那都是编译器检查出来的.所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译时类型检查,或静态类型检查
  • 运行时就是代码跑起来了,被装载到内存种了,⽽运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做些操作,做些判断.

是由汇编cc++构成的一套为Objective-C提供运行时的API

1、版本

Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)。

  • 早期版本对应的编程接⼝:Objective-C 1.0,用于32位的Mac OS X的平台上;
  • 现⾏版本对应的编程接⼝:Objective-C 2.0,iPhone程序和Mac OS X v10.5 及以后的系统中的 64 位程序。

未命名文件.png

2、调起方式

runtime有3种调起方式:

  • OC层面:[[NSObkect alloc] init]
  • NSObject类中定义的方法:performSelector:withObject:
  • Runtime API:objc_msgSend(...);

二、方法本质

我们先创建一个类,并添加方法,如图:
截屏2021-09-07 下午4.05.38.png
然后clang:clang -rewrite-objc main.m -o main.cpp
打开生成的main.cpp并找到main函数入口,如图:
截屏2021-09-07 下午4.05.20.png
稍微简化一下,如图:
截屏2021-09-07 下午4.08.20.png
不难发现,所谓的方法在底层就是objc_msgSend消息的发送。参数为消息接收者psel及参数。

结论:方法的本质就是objc_msgSend

三、objc_msgSend

我们已经发现方法的本质就是objc_msgSend,现在我们来探究一下。

1、源码分析

首先我们进入objc_msgSend,如图:
截屏2021-09-07 下午4.19.50.png
到这里我们就发现无法进入实现,这里就要想一下:runtime是由汇编cc++构成的一套为Objective-C提供运行时的API 所以我们可以找汇编的实现,如图:
截屏2021-09-07 下午4.28.57.png

23个文件中645个结果,有点吓人。23个文件中645个结果,有点吓人。但是记住一点 我们找的是汇编,是.s文件,简单处理一下,如图:
2B6885AA-18A0-4F2F-A8F8-931A4EB56E3B.png
这些.s文件都是objc_msgSend的实现,根据cpu架构的不同汇编的语法也有所不同,但是实现的过程基本都是一样的,我们选择arm64的进行分析,如图:
截屏2021-09-07 下午4.40.07.png
打开之后发现也还是好多,此时找到ENTRY _objc_msgSend,如图:
E81319F8-1AE0-46F8-A284-53D80DE88292.png
大致流程:

  1. 判断:
  1.1 接收者是否为空;
  1.2 判断小对象,如果小对象为空,则直接返回空,即LReturnZero;是小对象执行LNilOrTagged;
  1.3 LNilOrTagged再次判断接收者是否为空,空则直接返回空,即LReturnZero;非空则GetTaggedClass,然后跳转到LGetIsaDone;
  1.4 既不是小对象,接受者也非空就会GetClassFromIsa_p16,获取class并存在p16
复制代码

GetClassFromIsa_p16,如图:
截屏2021-09-08 上午9.19.52.png
稍微的对注释进行了翻译。

  1. LGetIsaDone

    这里有一个宏定义方法: CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    分为:NORMAL(正常缓存查找)、_objc_msgSend__objc_msgSend_uncached (没有找到缓存),对应的定义,如下:

.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

//一旦我们超过了LLookupStart\Function标签,我们就可能加载了一个无效的缓存指针或掩码。

//当task_restartable_ranges_synchronize()被调用时,
//(或者当一个信号击中我们)在我们经过LLookupEnd\Function之前,
//然后我们的PC将被重置为LLookupRecover\Function,
//它会强制跳转到cache-miss代码路径,代码路径有以下要求:

//GETIMP:
//缓存未命中只是返回NULL(将x0设置为0)

//正常和查找:
//- x0表示接收端
//-x1包含选择器
//- x16包含isa
//-其他寄存器按照调用约定设置

mov x15, x16 // stash the original isa 将x16的值(isa),赋值给x15
复制代码

缓存查找(快速查找)

LLookupStart\Function:
// p1 = SEL, p16 = isa => isa -> 类的 isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
  ldr p10, [x16, #CACHE] // p10 = mask|buckets
  lsr p11, p10, #48 // p11 = mask
  and p10, p10, #0xffffffffffff // p10 = buckets
  and w12, w1, w11 // x12 = _cmd & mask
  
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // x16(isa) 平移 16 就得到了 cache的地址(即为_bucketsAndMaybeMask的地址)
    // 将 _bucketsAndMaybeMask的地址存入 p11 寄存器中。
  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
  // 如果_bucketsAndMaybeMask第 0 位不等于 0,就跳转到 LLookupPreopt\Function
  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
  // 通过哈希求 index 下标 赋给p12
  and p12, p1, p11, LSR #48 // x12 = _cmd & mask
  
#endif // CONFIG_USE_PREOPT_CACHES

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
  ldr p11, [x16, #CACHE] // p11 = mask|buckets
  and p10, p11, #~0xf // p10 = buckets
  and p11, p11, #0xf // p11 = maskShift
  mov p12, #0xffff
  lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
  and p12, p1, p11 // x12 = _cmd & mask
  
#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 不匹配跳转到3
                         // } else {
2: CacheHit \Mode // hit: call or return imp  缓存命中
                         // }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;如果sel==0 没有命中,跳转到 MissLabelDynamic
   cmp p13, p10          // } while (bucket >= buckets) 判断是否越界了
   b.hs 1b //再哈希跳转到1
   
    // wrap-around:
    //   p10 = first bucket 第一个桶子
    //   p11 = mask (and maybe other bits on LP64) 掩码
    //   p12 = _cmd & mask 通过哈希求 index 下标 赋给p12
    //
    // 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.
    //
    //完全缓存可以通过CACHE_ALLOW_FULL_UTILIZATION实现。
    //当我们绕回第一个探测的桶时停止
    //而不是再次击中第一个桶。
    
    // 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
  add p13, p10, w11, UXTW #(1+PTRSHIFT)
                    // p13 = buckets + (mask << 1+PTRSHIFT)

#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
    add p13, p10, p11, LSL #(1+PTRSHIFT)
            // p13 = buckets + (mask << 1+PTRSHIFT)
#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
   LLookupEnd\Function:
   LLookupRecover\Function:
b \MissLabelDynamic  
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
   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 
  b.ne \MissLabelConstant // cache miss 缓存未命中 跳转到慢速查找
  sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
  SignAsImp x0
  ret
  
.else
  b.ne 5f // cache miss
  sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
  
.if \Mode == NORMAL
  br x17
  
.elseif \Mode == LOOKUP
  orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
  SignAsImp x17 //IMP returned in x17 (imp 返回在x17中)
  ret

.else
  .abort  unhandled mode \Mode
.endif
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

.endmacro
复制代码

命中缓存CacheHit

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

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
  TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp 检查并调用
.elseif $0 == GETIMP
  mov p0, p17
  cbz p0, 9f // don't ptrauth a nil imp
  // authenticate imp and re-sign as IMP 检查并重签imp
  AuthAndResignAsIMP x0, x10, x1, x16 
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
复制代码

AuthAndResignAsIMP

.macro AuthAndResignAsIMP
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// note: assumes the imp is not nil 假设imp不为空
eor $1, $1, $2 // mix SEL into ptrauth modifier
eor $1, $1, $3  // mix isa into ptrauth modifier
autib $0, $1 // authenticate cached imp 检查缓存imp
ldr xzr, [$0] // crash if authentication failed 检查失败
paciza $0 // resign cached imp as IMP 重签imp
.endmacro
复制代码

慢速查找__objc_msgSend_uncached
在这里调用MethodTableLookup,内容如下:

.macro MethodTableLookup

    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1 接收者和sel在x0和x1中

    mov x2, x16
    mov x3, #3
    //跳转到 _lookUpImpOrForward
    bl _lookUpImpOrForward

    // IMP in x0 找imp 在x0中
    
    mov x17, x0


    RESTORE_REGS MSGSEND

.endmacro
复制代码

在当前文件中找不到_lookUpImpOrForward,全局搜索,在objc-runtime-new.mm中找到了实现,接下来我们又回到了c++中开始慢速查找,lookUpImpOrForward的实现如下:

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();

    if (slowpath(!cls->isInitialized())) {

        // The first message sent to a class is often +new or +alloc, or +self

        // which goes through objc_opt_* or various optimized entry points.

        //

        // However, the class isn't realized/initialized yet at this point,

        // and the optimized entry points fall down through objc_msgSend,

        // which ends up here.

        //

        // We really want to avoid caching these, as it can cause IMP caches

        // to be made with a single entry forever.

        //

        // Note that this check is racy as several threads might try to

        // message a given class for the first time at the same time,

        // in which case we might cache anyway.

        behavior |= LOOKUP_NOCACHE;

    }


    // runtimeLock is held during isRealized and isInitialized checking

    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make

    // method-lookup + cache-fill atomic with respect to method addition.

    // Otherwise, a category could be added but ignored indefinitely because

    // the cache was re-filled with the old value after the cache flush on

    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like

    // a class but really isn't one and do a CFI attack.

    //

    // To make these harder we want to make sure this is a class that was

    // either built into the binary or legitimately registered through

    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.

    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

    // runtimeLock may have been dropped but is now locked again

    runtimeLock.assertLocked();

    curClass = cls;

    // The code used to lookup the class's cache again right after

    // we take the lock but for the vast majority of the cases

    // evidence shows this is a miss most of the time, hence a time loss.

    //

    // The only codepath calling into this without having performed some

    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {

        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {

#if CONFIG_USE_PREOPT_CACHES

            imp = cache_getImp(curClass, sel);

            if (imp) goto done_unlock;

            curClass = curClass->cache.preoptFallbackClass();

#endif
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }
        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
    // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;

}
复制代码

未完待续!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享