iOS 底层第8
天的学习。今天的内容是分析objc_msgSend
有点难,需要一步步去分析,最后得出一个流程。
objc_msgSend 分析
- 在底层源码中搜索
objc_msgSend
找到arm64
架构下的objc-msg-arm64.s
文件,点击进入开始分析汇编。 - 开始分析前说明一下:所有代码分析都是针对
arm64
架构的。
- 接下来我们开始分析
cmp p0, #0
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 补充 LNilOrTagged
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
p0 = receiver
,cmp
判断消息接受者是否存在,#0
= 当消息接受者不存在 进入直接返回
LReturnZero
。 这里SUPPORT_TAGGED_POINTERS
在arm64
就返回1
,也就是会进入LNilOrTagged
,而LNilOrTagged
也会返回LReturnZero
->LGetIsaDone
说明已经找到了isa
.
- 当消息接受者存在的时候继续走下面的代码
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
ldr p13, [x0]
将存储器地址为x0
的字数据读入寄存器p13
。p13
-> 对象的首地址->isa
,x0
->isa
GetClassFromIsa_p16 p13, 1, x0
把参数传入GetClassFromIsa_p16
搜索GetClassFromIsa_p16
找到macro
定义
.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
GetClassFromIsa_p16
参数表
参数名 | 参数传入的值 |
---|---|
src |
p13(isa) |
needs_auth |
1 |
auth_address |
x0(isa) |
- 我们看下
SUPPORT_INDEXED_ISA
这个判断条件
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
1:
__ARM_ARCH_7K__
根据名称可以得出是定义在目标为 ARM 7k 架构 CPU ,这里应该主要是针对watchOS
的,不满足条件。2:
(__arm64__ && !__LP64__)
在真机环境__arm64__
=1
,__LP64__
=1
,1 && 0
=0
,因此SUPPORT_INDEXED_ISA = 0
继续往下分析
needs_auth = 0
肯定不成立,我们看GetClassFromIsa_p16
参数表可知needs_auth =1
.if
判断不成立 =>ExtractISA p16, \src, \auth_address
,把p16
(空地址),src
,auth_address
传入到ExtractISA
。接着全局搜索ExtractISA
// A12 不是普遍版本
#if __has_feature(ptrauth_calls)
// JOP
// ... 省略部分代码
.macro ExtractISA
and $0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
xpacd $0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
mov x10, $2
movk x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
autda $0, x10
#endif
.endmacro
// JOP
#else
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
// not JOP
#endif
-
#if __has_feature(ptrauth_calls)
只有A12
架构会进入,不是普遍版本,直接看#else
下面的代码
and $0, $1, #ISA_MASK
参数表
参数名 | 参数传入的值 |
---|---|
$0 |
p16 |
$1 |
src = p13(isa) |
-
and $0, $1, #ISA_MASK
=>isa & ISA_MASK = p16
- 就这样我们就得到了
GetClassFromIsa_p16 p13, 1, x0
=>p16 = class
- p16 = class
- 继续往下进行分析
LGetIsaDone: // 函数名
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
- 搜索
CacheLookup
如下
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
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, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
CacheLookup
参数表
参数名 | 参数传入的值 |
---|---|
Mode |
NORMAL |
Function |
_objc_msgSend |
MissLabelDynamic |
__objc_msgSend_uncached |
MissLabelConstant |
nil = MissLabelConstant |
-
mov x15, x16
, 把p16 isa
移到x15
,x16 = p16
, -
LLookupStart\Function:
查找Function = _objc_msgSend
- 在
arm64
架构下直接看#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
判断下的代码 -
ldr p11, [x16, #CACHE]
,CACHE
多少不知道,搜索找到如下代码
#define CACHE (2 * __SIZEOF_POINTER__) // __SIZEOF_POINTER__ = 8, CACHE = 16
-
ldr p11, [x16, #CACHE]
=>x16 class
平移16
找到cache_t
, 把cache_t
存到p11
里。 p11 = cache_t
-
#if CONFIG_USE_PREOPT_CACHES
是多少,搜索下
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
-
arm64
=>CONFIG_USE_PREOPT_CACHES
=1
,继续看代码 -
#if __has_feature(ptrauth_calls)
只有A12
架构会进入,直接进入#else
-
and p10, p11, #0x0000fffffffffffe
=>p11 & #0x0000fffffffffffe
=>buckets
, p10 = buckets
-
tbnz p11, #0, LLookupPreopt\Function
——> 当前的p11
与#0
作比较,不等于0
就跳转LLookupPreopt
. - 继续分析,已经知道
p10 = buckets
,我们的目的是为了找到buckets
里的bucket
,再根据x1 = sel
与bucket
里的sel
进行比对, 找到相应的imp
.
eor p12, p1, p1, LSR #7
and p12, p1, p11, LSR #48`
-
eor p12, p1, p1, LSR #7
=>_cmd ^ _cmd >> 7
=value ^= value >> 7
- 为什么要在读的时候
_cmd
要右移7
呢?
- 因为在
insert
时计算begin
的时, 在cache_hash
函数value ^= value >> 7
,所以在读的时候也要进行相同的操作。 -
p11, LSR #48
=>p11(cache_t)
右移48
得到了mask
.
- 为什么 要右移
48
位就得到了mask
呢?
- 因为 总共
64
位,前16
位存了 mask ,后 48 位存了buckets
,当右移动48
位 就把buckets
移除了,所以 右移48
得到了mask
. - ->
and p12, p1, mask
->p1(_cmd ^ _cmd >> 7) & mask = p12
. - 那这个
p12
存的是什么呢?回到cache_t
中的源码进行查看
// insert 函数里 计算 begin
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// cache_hash 方法
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);
}
- 分析源码得知在
insert
时候begin = cache_hash(sel, m);
根据mask
后得到的begin
, 和p1 & mask
=p12
其实是计算index
, 会根据index
去找bucket
. - 当我们得到
p12
存了index
后继续往下分析
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
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
-
add p13, p10, p12, LSL #(1+PTRSHIFT)
,搜索已知PTRSHIFT = 3
,p10 = buckets
- =>
p12, LSL #(1+PTRSHIFT)
=index << 4
=> 可以理解为地址的平移, 相当于buckets[i]
- 为什么
index << 4
要左移4
位呢?
- 举个
index = 2
index = 2
index << 4 => 00 10 << 4 = 10 0000 = 32
32 = 2 * 16 => 向左平移了 2 个 bucket(sel,imp) 的大小
平移的目的就为了找到 bucket
- =>
add p13, p10,(index << 4)
=>p13 = buckets[i] = bucket
p13 = bucket
// 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
- 这里
1:
,2:
,3:
就相等于一个do white
循环 -
1: ldp p17, p9, [x13], #-BUCKET_SIZE
,[x13] = bucket
进行#-BUCKET_SIZE = bucket--
,ldp
就是将[x13]
偏移BUCKET_SIZE = 16
个字节的值取出来,放入p17
和p9
。这里的p9 = sel
,p17 = imp
. -
cmp p9, p1
=> 比较sel
与_cmd
- 如果不相同调用
b.ne 3f
就会调用3:cbz p9, \MissLabelDynamic
-
3:cbz p9, \MissLabelDynamic
=> 寻找p9
,如果p9 = 0
调用MissLabelDynamic = __objc_msgSend_uncached
-
- 如果相同调用
CacheHit \Mode
,搜索下CacheHit
找到macro
- 如果不相同调用
.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
- 参数
Mode = NORMAL
,$0 == NORMAL = true
-
x17 = imp
,x10 = buckets
,x1 = sel
,x16 = isa = class
TailCallCachedImp x17, x10, x1, x16
- 搜索
TailCallCachedImp
继续分析
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0
.endmacro
eor $0, $0, $3
=>$0 = cached imp
,$3 = isa
,imp
^isa
=>imp code
再存入$0
br $0
=>call imp
继续
cmp p13, p10
=>p13 = bucket
与p10 = buckets 首地址
进行比较,如果bucket >= buckets
调用b.hs 1b
继续进行内存平移。如果
bucket
已经走到了0
好位置,还不相等 怎么办呢?继续往下分析
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
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
-
buckets + (mask << 1+PTRSHIFT)
=>mask
向左移动了4
位置 =>7 * 16
=>p13
定位到最后一个的位置。 -
add p12, p10, p12, LSL #(1+PTRSHIFT)
=>p12 = first probed bucket
ccmp p13, p12, #0, ne // bucket > first_probed)
- 为什么
bucket > first_probed
?
因为之前已经比较过一次,所以这里就必须大于
first_probed
否则还要走一次进行比较以上分析就是
sel
找imp
的过程
objc_msgSend 流程
-
objc_msgSend
主要是通过SEL _cmd
找到bucket
里sel
,通过compare
最后锁定到imp
,这一步步的查找过程到底是如何的,我们在这里画个流程图再具体梳理下