objc_msgSend()汇编核心探索(x86_64架构)

寄存器对应须知:
函数参数寄存器(%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_uncachedlookupImpOrForward ---> _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_64arm64等:cache->buckets中的最后一个bucketEnd marker(尾标记),其bucket = {(SEL)(uintptr_t)1, (IMP)buckets},意味着buckets遍历到头了,同时也可快速继续从头遍历;End marker可参看Runtime的buckets::allocateBuckets()函数。
(3)switchcase 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扩容)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容