iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析
Posted 卡卡西Sensei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析相关的知识,希望对你有一定的参考价值。
1. 回顾
在上篇博客iOS底层探索之Runtime:01—运行时&方法的本质中介绍了 运行时
和 编译时
的概念。同时也知道了OC
方法的调用,本质上是发送消息
,在底层通过objc_msgSend
方法来实现。那么底层是如何实现的呢?
2. 消息发送底层如何实现
补充1
Runtime
有两个版本 ⼀个是Legacy
版本(早期版本) ,另一个是Modern
版本(现⾏版本)
- 早期版本对应的编程接⼝:
Objective-C 1.0
- 现⾏版本对应的编程接⼝:
Objective-C 2.0
- 早期版本⽤于
Objective-C 1.0
,32位
的Mac OS X
的平台上 - 现⾏版本:
iPhone
程序和Mac OS X v10.5
及以后的系统中的64 位
程序
Objective-C Runtime Programming Guide
下面的代码已经不陌生了吧!调用效果都是一样的,一个是上层OC
的对象调用方法,一个是下层消息的发送
。
JPStudent *stu = [[JPStudent alloc]init];
[stu test];
objc_msgSend(stu, sel_registerName("test"));
sel_registerName
是一个C
语言的方法,传入一个C
语言的字符串
(其实就是我们的方法名称)
objc_msgSend(<#id _Nullable self#>, <#SEL _Nonnull op, ...#>)
sel_registerName(<#const char * _Nonnull str#>)
//C语言的函数,闯入一字符串
sel_registerName("test")
等价于 @selector(test)
,我们可以打印下它们的地址。
NSLog(@"%p---%p",sel_registerName("test"),@selector(test));
//打印输出
2021-06-29 12:58:50.610720+0800 方法的本质探索[42704:741799] 0x7fff7b9f5ddc---0x7fff7b9f5ddc
从打印结果来看,是一模摸一样样😁
补充2
OC
中方法的调用,底层都是转换为消息发送objc_msgSend
函数的调用,执行流程大概可以分为三大阶段。
- 消息发送流程
- 动态方法解析流程
- 消息转发流程
2.1 查找源码
既然要看objc_msgSend
的底层,就得去苹果的源码里面去看看,必须要深入底层去探索。
源码工程查找
objc_msgSend
我的天哪!什么鬼👻啊?这么多文件,有汇编
的,有C/C++
的该看哪一个呢?而且架构还不一样。
我们肯定是要找
arm
架构的,不要问为什么,问就是找它就对了,哈哈!因为我们手机的真机是arm
架构的,加上OC的底层都是C、C++和汇编实现的,所以我们基本可以定位到objc-msg-arm64.s
这个文件。
2.2 查看源码
既然找到了,就不要在外面停留了,进去看看。
偶买噶,我的天那!这是熟悉又陌生(大学学过)的汇编啊!恶魔😈,噩梦啊!大学学的时候就很懵!
靓仔,稳住,挺住!
汇编确实是比较难啃,但也不是啃不动,一口吃不下,就慢慢啃!干,就完了!
3. 分析汇编
汇编源码是从
ENTRY _objc_msgSend
开始,到END_ENTRY _objc_msgSend
结束。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
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
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
3.1 _objc_msgSend
p0
和空对比,即判断接收者是否存在,其中p0
是objc_msgSend
的第一个参数-消息接收者receiver
if else
判断,如果支持tagged pointer
,跳转至LNilOrTagged
,如果小对象为空,则直接返回空,即LReturnZero
。如果小对象不为空,则处理小对象的isa
,走到CacheLookup NORMAL
GetClassFromIsa_p16
是定义的一个宏,通过isa
找到对应的类,ExtractISA
也是个宏定义,将传入的isa&isaMask
,得到class
,并将class
赋给p16
GetClassFromIsa_p1
的宏定义
// p13(isa), 1, x0(isa)
//GetClassFromIsa_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
.else
// 64-bit packed isa
ExtractISA p16, \\src, \\auth_address
.endif
#else
// 32-bit raw isa
mov p16, \\src
#endif
.endmacro
ExtractISA
宏定义
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
3.2 CacheLookUp
CacheLookUp
核心代码
// 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
- 通过
cache
首地址平移16
字节(因为在objc_class
中,首地址距离cache
正好16
字节,即isa
占8
字节,superClass
占8
字节),获取cahce
,cache
中高16
位存mask
,低48
位存buckets
,即p11 = mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
- 从
cache
中分别取出buckets
和mask
,并由mask
根据哈希算法
计算出哈希下标
。在arm64
环境下,mask
和buckets
放在一起共占用8
个字节,64
位;其中mask
在高16
位,buckets
在低48
位。通过掩码
(0x0000fffffffffffe)与运算(&
)将高16
位抹零获取buckets
;将buckets
赋值给p10
。将cache
右移48
位,得到mask
,即p10 = buckets
。
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
// 走该流程获取buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\\Function
#endif
// 此部分就位cache_hash算法
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
- 将
objc_msgSend
的参数p1
(即第二个参数_cmd
)& msak
,通过哈希算法,得到需要查找存储sel-imp
的bucket
下标index
,即p12 = index = _cmd & mask
。这是因为系统在存储sel-imp
时,就是通过哈希计算得到下标,再去存储,所以读取也需要通过同样的方式。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
通过首地址 + 实际偏移量,获取哈希下标
index
对应的bucket
- 知道了下标,
buckets
的首地址也有了,那么怎么找到_cmd
的位置呢?我们都知道可以通过内存地址平移,在bucket_t
中存放的是imp
和sel
,8+8=16
个字节。
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
根据buckets
首地址偏移下标 乘以16
个单位,其中PTRSHIFT = 3
,相当于下标左移4
就是以16
的倍数进行平移
,然后加上buckets
首地址的话,就获得了当前_cmd
对应的bucket
地址。根据获取的bucket
,取出其中的sel
存入p17
,即p17 = sel
,取出imp
存入p9
,即p9 = imp
。
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
cmp p9, p1
,如果当前获取的sel
与要查找的sel
相同,则缓存命中,CacheHit
。- 如果不相等,则进入
3
流程中,判断当前获取的sel,p9
是否为空,如果为空,则Miss
,缓存没有命中。 - 如果获取的
sel
不为空,说明存在下标冲突,则以当前获取的bucket
的地址与首个bucket
的地址进行比较如果获取地址,大于等于首地址,继续比较流程,向前查找,循环下去!直到查询到首地址位置。 - 如果上面的循环结束依然没有找到,则会进入下面的流程,
CACHE_MASK_STORAGE_HIGH_16
环境下,同样p11
右移48
位获取mask
,而mask
等于开辟的总空间容量减1
,所以获取最后一个存储空间所在的位置,也即是首地址的基础上,添加mask*16
的位置,所以这里p13
就是当前最大的那个存储空间,也就是最后一个存储空间。
#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
上面已经知道p12
是要查找方法_cmd
的存储下标,只要把首地址加上偏移地址index*16
,就可以知道_cmd
对应bucket
地址,并赋值给p12
。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
此次循环是从最后一个位置,查找的
_cmd
对应位置,进行向前查找
#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
cmp p9, p1
,如果当前获取的sel
与要查找的sel
相同,跳转至流程2,即缓存命中,CacheHit
如果不相等,判断sel
是否为空,如果不为空,并且循环获取的地址大于p12
的位置,继续循环流程。
如果以上流程均未能命中缓存,则进入MissLabelDynamic
流程
3.3 CacheHit
下面是缓存命中(
CacheHit
)的分析
// 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
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
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
// 调用imp
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0
.endmacro
CacheLookup
中,Mode
传入的为NORMAL
,会执行TailCallCachedImp
,在TailCallCachedImp
实现中进行了位异或运算,获取imp
。因为在存储imp
时,对imp
进行了编码处理,取出执行调用时,需要进行解码操作。
如果缓存没有命中,则会进入MissLabelDynamic
流程。全局搜索MissLabelDynamic
,发现MissLabelDynamic
即为CacheLookUp
的第三个参数
// NORMAL, _objc_msgSend, __objc_msgSend_uncached , MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
就是
_objc_msgSend
中传入的__objc_msgSend_uncached
源码工程里面全局搜索__objc_msgSend_uncached
分析:
在该函数中执行宏MethodTableLookup
,继续跟踪MethodTableLookup
,在MethodTableLookup
的汇编实现中,我们可以看到最重要的_lookUpImpOrForward
的方法,然后全局搜索_lookUpImpOrForward
发现搜不到实现方法, 说明该方法并不是汇编实现的,需要去C/C++
源码中查找。
到此,
消息发送
流程中汇编快速查找
的分析就结束了,因为lookUpImpOrForward
不是汇编实现的,是C/C++
实现的,所以属于(慢速查找
)。lookUpImpOrForward
慢速查找下次再分析。
4. 总结
- 为什么底层不用C或者C++用汇编?
- 汇编更接近机器语言,直接操作寄存器,查找效率高
- 因为一些方法的参数未知,汇编可以处理未知的参数,更加动态化一点
objc_msgSend
函数的调用,执行流程大概可以分为三大阶段。
- 消息发送流程(1.汇编快速查找,2.慢速查找)
- 动态方法解析流程
- 消息转发流程
- 消息发送流程流程图:
补充
苹果2020 WWDC关于Runtime
的优化
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹
以上是关于iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析的主要内容,如果未能解决你的问题,请参考以下文章
iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析
iOS开发底层之RuntimeObjc_msgSend探究 - 08