objc-msg-arm64源码深入分析

在 Objective-C 语言中,实例对象执行方法,而执行方法的过程也可以称为给实例对象发送消息。发送消息的过程执行在编译阶段会转化成对 objc_msgSend 函数的调用。本文将分析 objc_msgSend 汇编部分主要部分(fast path)。

文章中用到的汇编指令可以参考我个人的汇编学习笔记

Objective-C 实例对象执行方法步骤

objc_msgSend 前2个传入参数有对象实例 receiver 和方法名 selector,执行过程可以简单概括为:

  1. 获取 receiver 对应的类 Class
  2. 在 Class 缓存列表中根据选择子 selector 查找 IMP
  3. 若缓存中没有找到,则在方法列表中继续查找
  4. 若方法列表没有,则从父类查找,重复以上步骤
  5. 若最终没有找到,则进行消息转发操作

objc_msgSend 汇编源码内部逻辑

    ENTRY _objc_msgSend     // _objc_msgSend 入口
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check, cmp 指令执行完,设置Z-flag(零标志)
    b.le    LNilOrTagged        // 如果 x0 的值==0,CPSR寄存器的 Z 标识==1,跳转标签判断是否 self 是否为 nil 或者是 tagged pointer 类型
                                //  跳转之前 lr 寄存器会保存 pc 寄存器当前内容
    ldr x13, [x0]       // x13 = isa,把 self 指针赋值到 x13,self 是 objc_object 结构体,结构体第一个属性是 isa,所以这里 x13 指向了 isa
    and x9, x13, #ISA_MASK  // x9 = class,与运算来移除掉这些多余的信息,将一个真实指向类的指针保存在 x9 里    
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:           // 执行到这里说明 self 的值等于 0。小于零则代表为 Tagged Pointer 情况,等于说明为 nil
    b.eq    LReturnZero     // nil check,判断 self 是否为 nil

    // 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    // 它从 self 中的第 60 位开始,提取 4 位,保存到 x11 中。
    ldr x9, [x10, x11, LSL #3]  // x9 = x10 + (x11<<3),这里通过 x11 里的索引到 x10 所指向的 Tagged Pointer 表中查找具体的类
    b   LGetIsaDone

LReturnZero:
    // x0 is already zero
    // 因为近来之前已经通过 `cmp x0, #0` 判断,所以 x0 寄存器的值是0

    // 整型的返回值保存在 x0 和 x1 中
    // 浮点型的返回值会被保存在 v0 到 v3 这几个向量寄存器中,
    // d0 到 d3这几个寄存器是相关v寄存器的后半部分,向他们存值的时候会将对应 v 寄存器的前半部分置 0
    
    mov x1, #0      // 1、首先先把 x1 清空,x0 这里是 self,已经是0,所以不需要清空,
    movi    d0, #0  // 2、清空 v 寄存器
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend

_objc_msgSend 函数可以分解2个主线

  1. receiver 为 nil 或者属于 tagged pointer 类型
  2. receiver 不为空,正常查找 IMP

receiver 不为空

首先先分析 receiver 不为空的情况

1. ldr  x13, [x0]
2. and  x9, x13, #ISA_MASK  
3. LGetIsaDone:
        CacheLookup NORMAL
  1. x0 当前存储的是 self 指针,ldr 指令 self 指针所指向的内存位置读取数据并保存到 x13 寄存器中,这时候 x13 存储了 isa

  2. isa 和 ISA_MASK 做与运算,移除掉这些多余的信息得到 Class 并存储到 x9

  3. 开始从 Class 缓存中查找 IMP

CacheLookup 是一个宏

.macro CacheLookup
    // x1 = SEL, x9 = isa
    // x9 保存着 objc_class 指针
    
    ...
    
.endmacro

进入宏之前,x1 保存了 SEL (ARM寄存器 x0-x7 寄存器是用来传递参数的,objc_msgSend 函数的前2个参数分别是 self 和 _cmd),还有之前处理得到的 isa 保存在 x9。

    ldp x10, x11, [x9, #CACHE]  // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)
  1. ldp x10, x11, [x9, #CACHE]:CACHE 是一个常数(0x10),以 objc_class 地址为基准,然后读 16 字节的数据(可以参考 objc-runtime-new.h objc_class 结构体),x10 = buckets,x11 = occupied|mask (高32位:occupied,低32位:mask);

    mask 代表哈希表的位数,它的值总是2 - 1的幂,或者用二进制表示就是000000001111111,末尾有一个可变的1

  2. and w12, w1, w11:进行 AND 运算,得到选择子的查询索引

  3. add x12, x10, x12, LSL #4: x12 左移4位也就是乘以16,这是因为每个哈希表的 bucket 是 16 字节,计算得出要搜索位置的 第一个 bucket 的地址并保存在 x12

    ldp x16, x17, [x12]     // {x16, x17} = *bucket
1:  cmp x16, x1         // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->cls == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x16, x17, [x12, #-16]!
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)
  1. ldp x16, x17, [x12]: 从 bucket 指针指向的内存地址读取数据,x16 存储要查找 bucket 中的 key(选择子),x17 存储了 IMP

  2. cmp x16, x1: 判断第一个 bucket 中的 sel 跟参数 _cmd 是否相同

    • 相同: 跳转到 CacheHit 继续执行,改标签中会执行指令 br x17,也就是执行 IMP
    • 不相同: 跳转到 CheckMiss 继续执行 cbz x16, __objc_msgSend_uncached_impcache,cbz 指令比较寄存器值是否等于0,如果是0则跳转;这里 x16 中记录了从 bucket 加载到的选择子。首先先将其与 0 进行比较,如果等于 0 则会跳转至 C 函数 __objc_msgSend_uncached_impcache 进行更复杂的查找
  3. cmp x12, x10: 判断当前的 bucket 指针是不是和数组 buckets 指针相同,相同则说明在列表头

  4. 判断当前 bucket 的位置:

    1. 如果 bucket == buckets,则把指针指向 buckets 列表尾

      b.eq  3f
      add   x12, x12, w11, UXTW #4
      ldp   x16, x17, [x12] 
      

      cmp 指令执行之后如果,如果 x12 - x10 == 0,csrp 寄存器 Z 标志位置位1,反之为0。

      b.eq 当 Z 标志位为 1,跳转到 3f,执行 add x12, x12, w11, UXTW #4

      x12 存储了 buckets 指针,指向了第一个 bucket,w11 是存储表的掩码,描述了表的大小,相加之后当前指针指向最后一个 bucket

    2. 如果 bucket != buckets,

      ldp   x16, x17, [x12, #-16]!
      b 1b          // loop
      

      x12-16 获取新的 bucket 地址并重新写入到 x12 中 (!符号代表寄存器回写),指向前一个 bucket,x16 存储要查找 bucket 中的 key(选择子 ,x17 存储了 IMP,然后重复之前的步骤

接着上面的4.1步骤,bucket 指针指向 buckets 列表尾

1:  cmp x16, x1         // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
        CheckMiss $0            // miss if bucket->cls == 0
        cmp x12, x10        // wrap if bucket == buckets
        b.eq    3f
        ldp x16, x17, [x12, #-16]!  // {x16, x17} = *--bucket
        b   1b          // loop

3:  // double wrap
        JumpMiss $0
  1. 判断当前 bucket 的选择子和传入参数 _cmd 是否相同,相同则跳转到 CacheHit 执行对应的 IMP,不相同则往下走
  2. 执行 CheckMiss 宏判断 bucket 的选择子是否为空,若未空跳转执行 __objc_msgSend_uncached_impcache C 函数
  3. cmp x12, x10 ,检查是否在 buckets 表头循环搜索完 或者 是hash碰撞,如果是则跳转到 JumpMiss,最终会执行 __objc_msgSend_uncached_impcache 函数执行,进行更复杂的查找
  4. 若步骤3不成立,则 bucket 指针前移,重复1-3的步骤

recever 不为空的情况下, objc_msgSend 全部过程分析到此完毕

receiver 等于 nil

LNilOrTagged:   // 执行到这里说明 self 的值小于等于 0。小于零则代表为 Tagged Pointer 情况,等于说明为 nil
    b.eq    LReturnZero     // nil check,判断 self 是否为 nil

objc_msgSend 开始会将利用 cmp 指令将 receiver 和 0 做比较,若结果是小于等于0则会跳转到 LNilOrTagged 执行。

若 receiver == 0,则跳转到 LReturnZero

LReturnZero:
    // x0 is already zero
    // 因为近来之前已经通过 `cmp x0, #0` 判断,所以 x0 寄存器的值是0

    // 整型的返回值保存在 x0 和 x1 中
    // 浮点型的返回值会被保存在 v0 到 v3 这几个向量寄存器中,
    // d0 到 d3这几个寄存器是相关v寄存器的后半部分,向他们存值的时候会将对应 v 寄存器的前半部分置 0
    
    mov x1, #0      // 1、首先先把 x1 清空,x0 这里是 self,已经是0,所以不需要清空,
    movi    d0, #0  // 2、清空 v 寄存器
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

这里先后把整形寄存器和向量寄存器都置为0,这样做的好处是:

objc_msgSend 不知道调用者希望获得什么类型的返回值,是一个整型?两个?还是浮点类型或是其他类型?把所有返回值的寄存器都覆盖为0,后面调用者不管是想得到整型还是浮点型,都是0值。

那么如果调用者需要的返回值类型不是属于整型/浮点型,比如是寄存器不够存储的,更大结构的返回值需要调用者在内存中分配合适的内存空间并把内存地址传入 x8,函数通过写入这块内存来返回值。

objc_msgSend 执行过程中并不知道 x8 内存,所以在 LReturnZero 中并没有清除内存。解决办法是编译器生成代码会 objc_msgSend 执行前用0填满这块内存。

Tagged pointer 处理

Tagged Pointer 通过在其最后一个 bit 位设置一个特殊标记,用于将数据直接保存在指针本身中。具体细节可以参考深入理解 Tagged Pointer

// 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    // 它从 self 中的第 60 位开始,提取 4 位,保存到 x11 中。
ldr x9, [x10, x11, LSL #3]  // x9 = x10 + (x11<<3)
b   LGetIsaDone
  1. 通过 adrp 指令计算 _objc_debug_taggedpointer_classes 表(存储可用的 Tagged Pointer 的类)的数据地址到当前pc寄存器值相对偏移。
  2. AMR64 上的指针是 64 位宽的,指令是 32 位宽。所以一个指令无法保存一个完整的指针,于是还要通过 add 指令把后半部分读取存储到 x10
  3. ubfx 指令读取 x11 中最后4位数据,也就是 Tagged pointer 表所以标志
  4. x9 = x10 + (x11<<3),这里通过 x11 里的索引到 x10 所指向的 Tagged pointer 表中查找具体的 Tagged pointer 类
  5. 获取到 isa 之后进行 CacheLookup 步骤

自此 objc_msgSend 过程已经全部分析完,整个流程可以用下图表示:

[图片上传失败...(image-e392dd-1563026243771)]

为什么要用汇编实现

objc_msgSend 函数实现并不是用 Objective-C、C 或者 C++ 实现的,而是利用汇编语言开发。

那为什么会采用汇编语言实现呢?首先看一个例子:

NSUInteger n = [array count];
id obj = [array objectAtIndex:1];

我们可以理解上面2行代码编译时期会转化为:

NSUInteger n = objc_msgSend(array,  @selector(count));
id obj = objc_msgSend(array, @selector(objectAtIndex:), 1);

假设 objc_msgSend 是 C 或者 C++ 实现的,这里不可能编译成功,因为返回值也不能同时是 NSUIntegerid;这里可以使用类型强制转化来解决:

NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array,  @selector(count));
id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);

从例子上可以看的出,objc_msgSend 有2个特点:

  1. 可以调用任意参数类型、数量的任意函数
  2. 支持不同类型的返回值

对于特点1,调用 objc_msgSend 的之前,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是完全一致的。

基于这个前提,遍历并找到 IMP 之后,只要所有的对栈、寄存器的操作回复到调用 objc_msgSend 之前的状态,通过 jump/call 指令执行函数即可。

在 ARM 上,IMP 函数执行完, r0 寄存器会保存其返回值,能满足其返回不同类型返回值的需求

参考文章:

为什么objc_msgSend必须用汇编实现

Dissecting objc_msgSend on ARM64

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

推荐阅读更多精彩内容

  • 概览 每个Objective-C对象都有相应的类,这个类都有一个方法列表。类中的每个方法都有一个选择子、一个指向方...
    alvin_wang阅读 724评论 0 0
  • objc_msgSend是OC中调用最为频繁的方法,所有OC方法的调用都离不开这个它。苹果已经将其开源(https...
    某某香肠阅读 733评论 0 0
  • 一.objc_msgSend函数简介 以前去面试,有人问了这个一个问题 发生了什么?一听这个问题,一脸懵逼。这不就...
    充满活力的早晨阅读 432评论 0 2
  • 天早已进入冬季,深圳的天气也开始微微凉了,听说,老家已经棉袄加秋裤,等待着过冬。深圳的气候太过于舒适,导致我一直都...
    殷勤说阅读 278评论 0 0
  • 茨威格的《列夫*托尔斯泰》是八年级下册第一单元的课文,之前也讲过一次,只是没怎么细致地分析,利用。今天读来,感觉实...
    骕棋朋薇阅读 1,820评论 0 2