7.ios-runtime 初探 _objc_msgSend 缓存快速查找流程

566 阅读5分钟

运⾏时 就是代码跑起来了.被装载到内存中去了 . (你的代码保存在磁盘上没装⼊内存之前是个死家伙.只有跑到内存中才变成活的).⽽运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做些操作,做些判断. 即:编译器编译时会做一些操作和判断。

image.png

  • runtime 的三种使用方式

      1. Objective-C code @selector()
      1. NSObject 的方法 NSSelectorFromString()
      1. sel_registerName 函数 api image.png
  • clang -rewrite-objc main.m -o main.cpp

  • oc中的代码 image.png

  • c++中的转化 image.png

  • // 调用方法 = 消息发送 : objc_msgSend(消息的接受者,消息的主体(sel + 参数))

  • [LGPerson alloc]; 等价于 objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));

  • [person sayNB]; 等价于 objc_msgSend(person, @selector(sayNB)); 等价于 objc_msgSend(person, sel_registerName("sayNB"));

  • 这里修改为NO才能通过编译 image.png

image.png

  • objc_super 结构体

    • receiver : 消息发送的接收者 如person 对象
    • super_class : 表示这个方法从哪个类中开始查找 image.png
  • 注意此时 person 继承自 teacher 最后调用到了父类的 sayHello 方法 image.png

_objc_msgSend 汇编源码分析

查找缓存前流程

image.png

  • 汇编

  • cmp p0, #0 // nil check and tagged pointer check p0寄存器中存放了函数的第一个参数即如:person对象,p0与#0 比较,消息接收者是否为空。

  • b.le LNilOrTagged // (MSB tagged pointer looks negative) 判断是否为 tagged pointer 小对象类型 b 小于或等于 nil 或则 小对象 Yes: 进入 LNilOrTagged操作,NO:向下走

  • b.eq LReturnZero 判断是否为空 YES:则跳转至 LReturnZero 进行操作 NO:向下走

  • ldr p13, [x0] // p13 = isa load 将[x0]数据写入到寄存器中 即:p13寄存器保存了 对象的isa

  • GetClassFromIsa_p16 p13, 1, x0 // p16 = class isa & 掩码 找到 class 将类地址存入到p16寄存器中。

    .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
    
    #if SUPPORT_INDEXED_ISA
            // Indexed isa
            mov	p16, \src			// optimistically set dst = src
            tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
            // isa in p16 is indexed
            adrp	x10, _objc_indexed_classes@PAGE
            add	x10, x10, _objc_indexed_classes@PAGEOFF
            ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
            ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
    1:
    
    #elif __LP64__
    .if \needs_auth == 0 // _cache_getImp takes an authed class already
            mov	p16, \src   -p13寄存中的数据移动到p16寄存器中
    .else
            // 64-bit packed isa
            ExtractISA p16, \src, \auth_address --- //将p13 & 掩码 后存入到 p16寄存器中,即:p16 存放了 class
    .endif
    #else
            // 32-bit raw isa
            mov	p16, \src
    
    #endif
    
    .endmacro
    
    
    
    
    .macro ExtractISA
    and    $0, $1, #ISA_MASK  //将 $1 & 掩码 后存入到 $0 中
    .endmacro
    
    
    • 上面的操作 通过 receiver 获取了 class :因为cache是存入在 class 中的,因为先找缓存中的方法
  • CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached //开始查找缓存

  • 总结上面的操作就是找到对象的类,放入到p16寄存器中。

上述流程图

objc_msgSend查找缓存前操作.png

CacheLookup 分析

// NORMAL, _objc_msgSend, __objc_msgSend_uncached , MissLabelConstant

  • 传入参数 NORMAL _objc_msgSend __objc_msgSend_uncached .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// NORMAL, _objc_msgSend, __objc_msgSend_uncached ,  MissLabelConstant
.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
	//
    
	mov	x15, x16			// stash the original isa  
LLookupStart\Function:
	// p1 = SEL, p16 = 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
	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

//  p11 cache -> p10 = buckets
//  p11, LSR #48 -> mask
//  p1(_cmd) & mask = index -> p12
	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
	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

// objc - 源码调试 + 汇编
//  p11 cache -> p10 = buckets
//  p1(_cmd) & mask = index -> p12
//  (_cmd & mask) << 4  -> int 1 2 3 4 5   地址->int
//  buckets +  内存平移 (1 2 3 4)
//  b[i] -> b + i
//  p13 当前查找bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//  *bucket--  p17, p9
//  bucket 里面的东西 imp (p17) sel (p9)
//  查到的 sel (p9) 和我们 say1
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
	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
	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

    1. mov x15, x16// stash the original isa ,将x16寄存器的值移动到x15寄存器
    1. 开始查找方法 image.png
    1. ldr p11, [x16, #CACHE] // p11 = mask|buckets //将 x16 寄存器的数据平移 #CACHE的大小后 存放到p11中。即:类class平移 0x10 找到了缓存地址 cache_t [x16, #CACHE] = cache_t = p11
    • #define CACHE (2 * SIZEOF_POINTER) // 2*指针的大小 = 16字节

image.png

image.png

    1. 真机环境进入这里 为了直接查看源码 这里直接查看模拟器架构的内容直接查看 第6步 and p10, p11, #0x0000fffffffffffe // p10 = buckets & 掩码 获取真正的地址 放到 p10 tbnz p11, #0, LLookupPreopt\Function //p11 与 0 作比较 缓存与0比较,测试位不为0发生跳转,为0 则继续往下走 进行查找。
    1. hash值计算下标index 存入 p12 这里是真机部分
    • 真机与模拟器之间的查边就在于
      • 真机计算hash值时进行了 右移7位 并 进行了 异或操作。
      • 模拟器则是直接 & mask
    • eor p12, p1, p1, LSR #7 //这个是真机架构下的操作
    • and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    • 这里与插入时计算hash值做同样的处理 image.png

image.png

// p1 = SEL, p16 = isa

ldr	p10, [x16, #CACHE]		// p10 = mask|buckets
lsr	p11, p10, #48			// p11 = mask #48就是 maskShit mask掩码
and	p10, p10, #0xffffffffffff	// p10 = buckets & bucket掩码
and	w12, w1, w11			// x12 = _cmd & mask


// objc - 源码调试 + 汇编
//  p11 cache -> p10 = buckets
//  p1(_cmd) & mask = index -> p12
//  (_cmd & mask) << 4  -> int 1 2 3 4 5   地址->int
//  buckets +  内存平移 (1 2 3 4)
//  b[i] -> b + i
//  p13 当前查找bucket
//  PTRSHIFT = 3
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//  *bucket--  p17, p9
//  bucket 里面的东西 imp (p17) sel (p9)
//  查到的 sel (p9) 和我们 say1
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                              //跳转到步骤1

总结

目标: objc_msgSend(receiver,_cmd) sel->imp //主要目的:通过方法名找到方法的地址

  • 1.receiver 是否存在
  • 2.receiver -> isa -> class(p16)
  • 3.class -> 内存平移 -> cache (bucket mask)
  • 4.bucket 掩码 -> bucket
  • 5.mask掩码 -> mask
  • 6.insert 哈希函数 (mask_t)(value & mask)
  • 7.第一次查找 index
  • 8.bucket+index 整个缓存里面的第几个bucket
  • 9.bucket{imp sel}
    1. sel == _cmd -> cacheHit -> imp ^ isa = imp(br) call 调用 imp
    1. 不相等 bucket -- 再次平移 找到下一个bucket ,即遍历查找
    1. 死循环 遍历
    1. 一直找不到 __objc_msgSend_uncached

流程图就拿别人的吧

image.png

我自己理解的流程图

objc_msgSend查找缓存操作.jpg

__objc_msgSend_uncached