寄存器对应须知:
函数参数寄存器(x0,x1,x2,x3,x4,x5,x6,x7)
p0~p17 ---> x0~x17
id objc_msgSend(id self, SEL _cmd,...)汇编实现
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//(1)`GetIsaFast `
cmp p0, #0 // nil check and tagged pointer check ;ZF = p0-0
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged //p0 <= 0(有符号数的比较) ,说明receiver要么为nil,要么就是个Tagged Pointer
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = a raw isa x0:实例对象的地址,[x0]:取8字节,那不就取到raw isa了嘛!
GetClassFromIsa_p16 p13 // p16 = isa :p13为a packed isa,需要进行class提取获得实例对象self的所属类。
//(2)`CacheLookup NORMAL, _objc_msgSend`
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
//(3)`LNilOrTagged `
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero //nil check:p0为nil,即receiver为nil,直接返回0结束objc_msgSend()的执行。
// tagged (系统定义的内部标签类)
// 这里加载了 _objc_debug_taggedpointer_classes 的地址,即 Tagged Pointer 主表
// ARM64 需要两条指令来加载一个符号的地址。这是 RISC 架构上的一个标准技术。
// AMR64 上的指针是 64 位宽的,指令是 32 位宽。所以一个指令无法保存一个完整的指针
adrp x10, _objc_debug_taggedpointer_classes@PAGE // 将页(前半部分)的基址存入 x10
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 将页(后半部分)的基址存入 x10
ubfx x11, x0, #60, #4 //从x0的第60位开始连取4位(即最后4位数据,代表具体类的“tag_slot”),存放到x11中。
ldr x16, [x10, x11, LSL #3] // x16 = [x10+(x11<<3)],到x10所指向的Tagged Pointer表中查找具体的类!
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone //如果不想等,说明是系统定义的标签类;否则,就是系统外部定义的标签类。
// ext tagged 相等: (外部标签类:非系统标签类)
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE //ext:外部的(标签类们)
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8 //连取8位(52~59)tag_slot
ldr x16, [x10, x11, LSL #3] //class = classes[tag_slot]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
//综合返回值:objc_msgSend函数的综合返回值,可能是整型(用x0和x1),也可能是浮点型(用v0,v1,v2,v3)
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0 //浮点型寄存器v0的低位,d0为0则整个v0都为0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
其汇编实现等价于C语言实现:
id objc_msgSend(id self, SEL _cmd,...){
(1)`NilTest`:对receiver(即self)进行非空测试。
if (!self) return;
(2)`LNilOrTagged`:receiver非空,获取其类cls的地址。
Class cls = self->getIsa();
(3)`CacheLookup NORMAL, _objc_msgSend`:遍历cls的缓存查找_cmd,如命中则返回其imp;未命中则开启慢查找。
IMP imp = CacheLookup(cls,_cmd);
if(imp) {
TailCallCachedImp imp, &imp, _cmd, cls; //汇编语句,arm64缓存查找的汇编显示只走到了这里。
} else {
(4) `__objc_msgSend_uncached` ---> `MethodTableLookup`--->
`lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);`:缓存未命中,开启慢查找 ---> 方法列表查找
imp = MethodTableLookup(cls,_cmd);
TailCallFunctionPointer imp; //汇编语句,arm64慢查找的汇编显示只走到了这里。
}
return 0;
}
注意:这里
CacheLookup
、TailCallCachedImp
、MethodTableLookup
、
TailCallFunctionPointer
皆为汇编宏。
其中,TailCallCachedImp
(CacheHit时)为:
.macro TailCallCachedImp //“尾部调用缓存IMP”:CacheHit-缓存命中
// $0 = cached imp, $1 = address of cached imp, $2 = _cmd, $3 = cls
eor $1, $1, $2 // mix SEL into ptrauth modifier
eor $1, $1, $3 // mix isa into ptrauth modifier
brab $0, $1
.endmacro
TailCallFunctionPointer
(慢查找命中时)为:
.macro TailCallFunctionPointer //“尾部调用函数指针”:CheckMiss-MethodTableHit(imp or forward_imp)
// $0 = function pointer value $0可能为真imp,亦可能为forward_imp
braaz $0
.endmacro
(1)GetIsaFast
+LNilOrTagged
的汇编实现
//(1)`GetIsaFast `
cmp p0, #0 // nil check and tagged pointer check ;ZF = p0-0
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged //p0 <= 0(有符号数的比较) ,说明receiver要么为nil,要么就是个Tagged Pointer
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = a raw isa x0:实例对象的地址,[x0]:取8字节,那不就取到raw isa了嘛!
GetClassFromIsa_p16 p13 // p16 = isa :p13为a packed isa,需要进行class提取获得实例对象self的所属类。
//(3)`LNilOrTagged `
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero //nil check:p0为nil,即receiver为nil,直接返回0结束objc_msgSend()的执行。
// tagged (系统定义的内部标签类)
// 这里加载了 _objc_debug_taggedpointer_classes 的地址,即 Tagged Pointer 主表
// ARM64 需要两条指令来加载一个符号的地址。这是 RISC 架构上的一个标准技术。
// AMR64 上的指针是 64 位宽的,指令是 32 位宽。所以一个指令无法保存一个完整的指针
adrp x10, _objc_debug_taggedpointer_classes@PAGE // 将页(前半部分)的基址存入 x10
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 将页(后半部分)的基址存入 x10
ubfx x11, x0, #60, #4 //从x0的第60位开始连取4位(即最后4位数据,代表具体类的“tag_slot”),存放到x11中。
ldr x16, [x10, x11, LSL #3] // x16 = [x10+(x11<<3)],到x10所指向的Tagged Pointer表中查找具体的类!
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone //如果不想等,说明是系统定义的标签类;否则,就是系统外部定义的标签类。
// ext tagged 相等: (外部标签类:非系统标签类)
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE //ext:外部的(标签类们)
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8 //连取8位(52~59)tag_slot
ldr x16, [x10, x11, LSL #3] //class = classes[tag_slot]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
以上汇编等价于Runtime C内联函数:
inline Class objc_object::getIsa()
{
//非TaggedPointer对象,直接获取其isa
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
// basic tagged
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
// extended tagged
cls = objc_tag_ext_classes[slot];
}
return cls;
}
(2)LGetIsaDone
,即CacheLookup NORMAL, _objc_msgSend
的汇编实现
// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = _cmd, x16 = cls
.macro CacheHit //命中则发起调用
.if $0 == NORMAL //Tail:尾部;尾巴
TailCallCachedImp x17, x12, x1, x16 //authenticate(证实...是真实的) and call imp
.elseif $0 == GETIMP //THINK:一个新类,首次调用了一个父类已缓存但是为forward_imp的方法sel
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp 缓存中为何会存在sel的imp为nil的情况?【待定。。。】
AuthAndResignAsIMP x0, x12, x1, x16 //authenticate imp and re-sign(签名) as IMP 认证并重新签名IMP
9: ret // return IMP 直接结束缓存查找并返imp为nil(p0)
.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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops //$0为非法值
.endif
.endmacro
//p9:从缓存bucket中取出的sel的值。
//进行未命中检查:p9是否为0?(未命中可以,但是sel不能为0!否则就直接宣布Cache中没有进行缓存了!)
//为0就意味着对应的bucket为nil,之前未插入缓存,说明未缓存!那就别再往下找了,缓存中没有!只能去方法列表中查找了!
.macro CheckMiss //未命中则进入MethodTable中查询!
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached //cbz指令:CBZ R, label,R为0则跳转至label处执行;否则不跳转。
.elseif $0 == LOOKUP //cbnz指令:CBNZ R, label R为非0则跳转至label处执行;否则不跳转。
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = cls
ldr p11, [x16, #CACHE] // p11 = mask|buckets mask(16) 0000 buckets(44)
//(arm64 && LP64)下,mask占据高16位,buckets占据低48位(高4位为0!)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets 用#0x0000 0ffffffffff也可以
and p12, p1, p11, LSR #48 // x12 = _cmd & mask ,x12为_cmd位于buckets中的插入起点下标begin。
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //(arm64 && !LP64)下
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. //!arm64下
#endif
//p10:buckets, p12:begin。
add p12, p10, p12, LSL #(1+PTRSHIFT) //PTRSHIFT为3,左移四位是因为每个bucket占16字节
// p12 = buckets + (begin << (1+PTRSHIFT)):_cmd所应该在缓存中的第一个位置地址!
//p12:第一个bucket的地址。
ldp p17, p9, [x12] // 从x12处连续读区两个8字节,imp--->p17、sel--->p9:arm64与x86_64在这点上是相反的。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0 如果sel为0,直接就未命中(miss)!
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f //相等则说明当前的selector在Cache中的起始下标为0!
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket 逆序查找!(x12取完后被赋了新值!)
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask ;p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT) 重置p12为Cache中【最后一个】bucket的地址!
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp 有值相等:命中
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0 无值为0:未命中
cmp p12, p10 // wrap if bucket == buckets 有值不等:下一个
b.eq 3f //Cache中必存在sel==0的bucket,所以缓存查找至多一圈(只有capacity为4时才会查一圈)!
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1: //一般情况下来不到这里!因为Cache中必存在sel==0的bucket,所以缓存查找至多一圈(含最后一个)。
LLookupRecover$1:
3: // double wrap 第二次遍历到buckets起点的bucket,一般情况下不会出现这种情况!
JumpMiss $0 //什么情况下才会来到这???强制进来时?【待定。。。】
.endmacro
以上汇编等价于C函数:
IMP CacheLookup(Class cls, SEL _cmd){
cache_t cache = cls->cache;
uintptr_t mab = cache._maskAndBuckets;
mask_t mask = mab >> 48;
bucket_t buckets = (mab <<16) >>16;
int bagin = _cmd & mask; //如果_cmd被缓存,那么它会先尝试放在buckets中的第bagin个位置。具体可看缓存的核心方法:void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
bucket_t bucket = buckets[bagin];
while (_cmd != bucket.sel) {
if (!bucket.sel) {
return (IMP)0; //直接返回0。实际上是进行`__objc_msgSend_uncached`,只不过这里单独列到了下一步。
} else if (bucket == buckets) { //第一次来到了起点,让bucket指向终点。
bucket = buckets[mask];
} else {
bucket--; //递减遍历
}
}
return bucket.imp;
}
附加:
__objc_msgSend_uncached
:CheckMiss时
//消息未缓存:Cache中之前未进行该sel的插入操作(即之前没调用过该方法或建立了新的缓存空间后没调用过!)
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup //去方法列表中查找吧!找到的imp放入x17中。
TailCallFunctionPointer x17 //找到imp后触发`TailCallFunctionPointer `
END_ENTRY __objc_msgSend_uncached
MethodTableLookup
:开启慢查找、解析、转发流程
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]! //压栈:将帧指针寄存器fp和链接寄存器lr压入栈中,然后sp = sp-16!
mov fp, sp //将fp指向栈底(成了基指针)
// save parameter registers: x0..x8(8字节), q0..q7(16字节)
sub sp, sp, #(10*8 + 8*16) //由高到低顺序:【】x8 x7 x6 x5 x4 x3 x2 x1 x0 q7 q6 q5 q4 q3 q2 q1 q0
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
//(x0, x1, x2, x3)
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16 //x16:类的地址cls(obj的isa)
mov x3, #3
bl _lookUpImpOrForward //带返回值(可返回继续执行)的跳转:跳转之前,将指令`mov x17, x0`的地址送入lr中。
// IMP in x0 imp可能为真,也可能为forward_imp(即(IMP)_objc_msgForward_impcache)
mov x17, x0 //子程序`_lookUpImpOrForward`调用结束并ret,返回值位于x0当中。
// restore registers and return 子程序`MethodTableLookup`调用结束,返回前恢复子程序调用前的寄存器状态!
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16 //恢复fp和lr,并重置sp = sp +16;
AuthenticateLR
.endmacro
总结:arm64
与x86_64
关于objc_msgSend()
汇编实现的不同之处主要在于缓存方面:
(1)缓存遍历方式:
arm64
为递减遍历,遍历至第0个后接着从最后一个往前遍历;x86_64
为递增遍历,遍历至最后一个后接着从第0个往后遍历;这实际上与它们缓存时的顺序是一致的。
(2)缓存临界值:arm64
可以存储3/4capacity个方法,总有1/4的空间不使用;x86_64
可以存储3/4capacity-1个方法,也总有1/4的空间不使用(减1是因为x86_64
有一个end Marker)。从而导致同样容量的缓存空间,x86_64
上界总比arm64
少一个。