寄存器对应须知:
函数参数寄存器(%rdi, %rsi, %rdx, %rcx, %r8, %r9)
64位 32位 16位 8位
%rax %eax %ax %al
%rdi %edi %di %dil
%r8 %r8d %r8w %r8b
#define a1 rdi //64位
#define a1d edi //32位
#define a1b dil //8位
#define a2 rsi //64位
#define a2d esi //32位
#define a2b sil //8位
#define a3 rdx //64位
#define a3d edx //32位
#define a4 rcx //64位
#define a4d ecx //32位
#define a5 r8 //64位
#define a5d r8d //32位
#define a6 r9 //64位
#define a6d r9d //32位
id objc_msgSend(id self, SEL _cmd,...)汇编实现
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
NilTest NORMAL (1)receiver为nil则直接ZeroReturn(带ret指令);否则继续往下走。
GetIsaFast NORMAL (2)r10 = self->isa 不为nil则获取receiver的isa,即所属类cls的地址
(3) calls IMP on success OR LCacheMiss_objc_msgSend 要么执行imp,要么缓存未命中跳至LCacheMiss_objc_msgSend执行
CacheLookup NORMAL, CALL, _objc_msgSend
(4)返回前清空%rax和%rdx等返回型寄存器:因为整型运算至多使用这两个寄存器存放返回值。清空后函数就返回了。
// 那为什么要清空呢?因为_objc_msgSend是imp查找、执行流,不需要返回值。
NilTestReturnZero NORMAL //ZeroReturn(带ret指令)
GetIsaSupport NORMAL (5)这条语句似乎不会执行啊。。。
//cache miss: go search the method lists
LCacheMiss_objc_msgSend:
// isa still in r10
jmp __objc_msgSend_uncached //进入慢速查找:Method Lists ---> lookupImpOrForward
END_ENTRY _objc_msgSend
其汇编实现等价于C语言实现:
id objc_msgSend(id self, SEL _cmd,...){
(1)`NilTest NORMAL`:对receiver(即self)进行非空测试。
if (!self) return;
(2)`GetIsaFast NORMAL`:receiver非空,获取其类cls的地址。
Class cls = self->getIsa();
(3)`CacheLookup NORMAL, CALL, _objc_msgSend`:遍历cls的缓存查找_cmd,如命中则返回其Imp;未命中则开启慢查找。
IMP imp = CacheLookup(cls,_cmd);
if (!imp) {
(4)`LCacheMiss_objc_msgSend` ---> `__objc_msgSend_uncached` ---> `MethodTableLookup NORMAL`--->
`lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);`:缓存未命中,开启慢查找 ---> 方法列表查找
imp = MethodTableLookup(cls,_cmd);
}
jmp *imp; //汇编语句:执行imp。
return 0;
}
注意:
(0)汇编“函数”与C/OC函数的互调:OC代码调用汇编“函数”时会给函数加上一个_
,如objc_msgSend
--->_objc_msgSend
,_objc_msgSend_uncached
--->__objc_msgSend_uncached
,lookupImpOrForward
--->_lookupImpOrForward
等;反过来会去掉_
,只是作以区分,知道就好。
(1)其中CacheLookup()
为表意函数,即假函数;MethodTableLookup()
也为表意函数,但MethodTableLookup
同时也为汇编宏;getIsa()
为真函数。
(2)总的来说,objc_msgSend()
为imp
查找、执行函数。它的返回值存放在了%rax
/%rdx
或%xmm0
/%xmm1
中,只不过都被设置为了0值,因为一般没人需要它的返回值。
Q1:objc_msgSend()
汇编实现中的GetIsaSupport NORMAL
似乎不会执行啊,没看懂。
对汇编的执行流程不甚了解,真诚求答。
Q2:对最后拿到的imp,arm64和x86_64为什么会有不同的处理方式?
arm64对最后的imp作了分类处里,而x86_64下则都是直接执行imp,难道慢查找返回的imp都是真的sel的imp吗?会不会是占位用的_objc_msgForward_impcache?
待回答。。。
(1)GetIsaFast NORMAL
的汇编实现
.macro GetIsaFast //快速获取Isa:receiver不是Tagged Pointer Object
.if $0 != STRET //对x86_64而言,LSB最低有效位为Tagged Pointer bit。
testb $$1, %a1b //Z=(%a1b & 1)?1:0,receiver最低位为1?是就是Tagged Pointer,否则就是普通对象
PN
jnz LGetIsaSlow_f //Z != 0:即为1,说明receiver为Tagged Pointer Object
movq $$ ISA_MASK, %r10 //Z = 0:将ISA_MASK:0x00007ffffffffff8ULL赋值给%r10
andq (%a1), %r10 //%r10 = %r10 & isa, %r10成了类地址(常识:打印一个对象就是打印对象地址)
.else
testb $$1, %a2b
PN
jnz LGetIsaSlow_f
movq $$ ISA_MASK, %r10
andq (%a2), %r10
.endif
LGetIsaDone:
.endmacro
//x86_64 Tagged Pointer Object的存储和表示:receiver的最低有效位LSB为1。
//(1)basic tagged:receiver的低4位(含LSB)为slot,去_objc_debug_taggedpointer_classes中找isa;
//(2)extended tagged:receiver的第4~11位为slot,去_objc_debug_taggedpointer_ext_classes中找isa;
.macro GetIsaSupport //慢速获取Isa:receiver是Tagged Pointer Object
LGetIsaSlow:
.if $0 != STRET
movl %a1d, %r11d //传送receiver的低4字节至%r11d
.else
movl %a2d, %r11d
.endif
andl $$0xF, %r11d //%r11d = %r11d & 0xF 获取receiver的低4位(basic tagged slot值)
// basic tagged
leaq _objc_debug_taggedpointer_classes(%rip), %r10 //%rip:存放当前CPU正在执行的指令的地址
movq (%r10, %r11, 8), %r10 // read isa from table 从basic tagged表中读取receiver对应的isa
leaq _OBJC_CLASS_$___NSUnrecognizedTaggedPointer(%rip), %r11 //slot为7(15)时对应的保留类。
cmp %r10, %r11
jne LGetIsaDone_b //cmp结果不为0:说明%r10即为receiver对应的isa!
// extended tagged 否则,%r10不是一个真正的类!需要从extended tagged表中读取receiver对应的isa。
.if $0 != STRET
movl %a1d, %r11d //传送receiver的低4字节至%r11d
.else
movl %a2d, %r11d
.endif
shrl $$4, %r11d //%r11d符号右移4位(去掉basic tagged的slot)
andl $$0xFF, %r11d //获取receiver的第4~11(8位)的值(extended tagged slot)
leaq _objc_debug_taggedpointer_ext_classes(%rip), %r10
movq (%r10, %r11, 8), %r10 // read isa from table 从extended表中读取receiver对应的isa
jmp LGetIsaDone_b //不需要再比较了,因为没有其他情况了!%r10即为receiver对应的isa!
.endmacro
以上汇编等价于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)CacheLookup NORMAL, CALL, _objc_msgSend
.macro CacheHit
// r11 = found bucket!
.if $1 == GETIMP //一级
movq cached_imp(%r11), %rax // return imp
cmpq $$0, %rax
jz 9f // don't xor a nil imp
xorq %r10, %rax // xor the isa with the imp
9: ret
.else
.if $1 == CALL //一级:objc_msgSend Normal,call %r10为类地址,%r11为_cmd在buckets中的地址。
movq cached_imp(%r11), %r11 // load imp 拿到了imp!
xorq %r10, %r11 // xor imp and isa 应该是反混淆
.if $0 != STRET //二级
// ne already set for forwarding by `xor`
.else
cmp %r11, %r11 // set eq for stret forwarding
.endif
jmp *%r11 // call imp !!!【最终直接发起调用】!!!
.elseif $1 == LOOKUP //一级
movq cached_imp(%r11), %r11
xorq %r10, %r11 // return imp ^ isa
ret
.else
.abort oops
.endif
.endif //这个endif貌似有点多余啊,它结束的是谁???
.endmacro
.macro CacheLookup //实际上整个缓存至多遍历一圈!因为sel的值为0时便会直接跳至LCacheMiss_objc_msgSend!
//而这样的情况总是会出现!因为对x86_64而言,缓存达3/4便会扩容!详情cache_t::insert()函数。
LLookupStart$2:
.if $0 != STRET
movq %a2, %r11 // r11 = _cmd 即sel(一个地址值)
.else
movq %a3, %r11 // r11 = _cmd
.endif //小字节序:isa -> super -> cache -> (mask/_flags/_occupied)
andl 24(%r10), %r11d //r11 = r11 & class->cache.mask 获取cache中的mask并执行位&得到begin
shlq $$4, %r11 // r11 = offset = r11<<4 每个bucket占16字节
addq 16(%r10), %r11 // r11 = class->cache.buckets + offset sel的begin的地址。
// 汇编代码下,是没有数据类型的概念的,指针只是一个数字,加1就是加1,而不是加“一个单位”!
.if $0 != STRET
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd) cached_sel为0,cached_imp为8。
.else
cmpq cached_sel(%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1f // scan more _cmd的地址与缓存中起始位置处的sel不相等!需继续扫描!
CacheHit $0, $1 // call or return imp //相等则为缓存命中
1:
// loop 当前bucket是否为cache的最后一个bucket:
cmpq $$1, cached_sel(%r11) //x86_64 Cache中的最后一个bucket的sel被置为1,imp指向第一个bucket
jbe 3f //YES(be:小于或等于)! if (bucket->sel <= 1) wrap(包裹) or miss(未击中;错过)
addq $$16, %r11 //NO, bucket++
2:
.if $0 != STRET
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd)
.else
cmpq cached_sel(%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more 往上跳
CacheHit $0, $1 // call or return imp
3: // wrap or miss wrap指的是Cache中的最后一个bucket“包裹”着第一个bucket!miss就是未占用的缓存bucket。
jb LCacheMiss$2 // if (bucket->sel < 1) cache miss :未占用的缓存bucket={0,0}
// LCacheMiss$2:LCacheMiss_objc_msgSend
// wrap 第一次遍历到了Cache中的最后一个bucket,尚未遍历一圈,还有可能缓存命中!请继续遍历cache!
movq cached_imp(%r11), %r11 //bucket->imp is really first bucket
//bucket->sel == 1:说明是最后一个bucket,其imp指向第一个bucket
jmp 2f //往下跳!
1:
// loop 为了代码的通用性,每次都要判断当前bucket是否为最后一个bucket。
cmpq $$1, cached_sel(%r11) //第二次比较:其主要目的看是否小于1,而不是等于1(因为不会再次等于1了)
jbe 3f // if (bucket->sel <= 1) wrap or miss
addq $$16, %r11 // >1:普通的bucket。 bucket++
2:
.if $0 != STRET
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd)
.else
cmpq cached_sel(%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more
CacheHit $0, $1 // call or return imp
3:
// double wrap or miss 第二次 wrap or miss 已遍历一圈了,就不用再遍历了!
jmp LCacheMiss$2 //对于x86_64而言,buckets中至少有一个bucket={0,0},所以来到这时,
//只意味着bucket={0,0},未命中!
LLookupEnd$2:
.endmacro
以上汇编等价于C函数:
IMP CacheLookup(Class cls, SEL _cmd){
cache_t cache = cls->cache;
bucket_t buckets = cache._buckets;
int bagin = _cmd & cache.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) {
switch ((uintptr_t)bucket.sel) {
case 0: //当前bucket为空,直接宣布未命中!
return nil; //实际上为`LCacheMiss_objc_msgSend`,只不过这里做了分步处理,将其独立出来了。
case 1: //当前bucket为缓存中的最后一个bucket,即Mark end bucket;
bucket = bucket.imp; //最后一个bucket的imp为buckets,即缓存的开始处。
break;
default:
bucket++;
break;
}
}
return bucket.imp;
}
注意:
(1)LCacheMiss$2
中的$2
为汇编调用中的第3个参数,前两个为$0
、$1
;这里的$2
为汇编调用者_objc_msgSend
,所以当缓存命中失败时会调用LCacheMiss_objc_msgSend
,类似字符串拼接,哈哈哈。
(2)对非arm
而言,如x86_64
、arm64
等:cache
->buckets
中的最后一个bucket
为End marker
(尾标记),其bucket = {(SEL)(uintptr_t)1, (IMP)buckets}
,意味着buckets
遍历到头了,同时也可快速继续从头遍历;End marker
可参看Runtime的buckets::allocateBuckets()
函数。
(3)switch
中case 0:
或case 1:
的情况至多出现一次!也即cache
至多能遍历一圈(而且还是capacity为4的情况),因为缓存中总是存在{0,0}
和{1,buckets}
这两个bucket,这是由缓存的设计机制所决定的。详情请看cache_t::insert()
函数(x86_64
:缓存达到3/4capacity -1时,再有方法进行缓存时便会Double扩容;arm64
:缓存达到3/4*capacity时再有方法进行缓存时便会Double扩容)。