-
剖析ARM64下的objc_msgSend
建议结合objc_msgSend源码来阅读本文。在了解objc_msgSend的原理的同时,也可作为ARM64汇编的入门。 本文结合原文评论区Greg Parker的评论略做修改。 原文:Dissecting objc_msgSend on ARM64 原文作者:Mike Ash 概述 每一个OC对象有一个类,每一个OC类都有一个方法列表。每一个方法都有一个selector,一个指向方法实现的函数指针,以及一些元数据。objc_msgSend的工作就是传入对象和selector,查找相应方法的函数指针,然后跳到函数指针所指向的位置。 查找方法的过程可能是非常复杂的。如果在一个类里没有找到这个方法,那么它会继续到superclass里去查找。如果在所有的superclass中都没有找到,就会调用运行时的消息转发代码。当一个类第一次收到消息时,他会去调用类的 +initialize方法。 通常查找一个方法必须是迅速的,因为每次消息的调用都需要有这个过程。这就和复杂的查找过程有冲突了,复杂但是要快。 OC解决这个冲突的方案是做方法缓存。每一个类有一个cache,用于存储方法的selectors和函数指针,也就是所谓的IMP。他们被组成一个哈希表,所以查找的时候是非常快的。当查找一个方法时,运行时首先询问cache。如果cache里没有这个方法,后续就会有一个缓慢而又复杂的过程,最后会把找到的结果放到cache里,这样下次查找该方法的时候就会很快了。 objc_msgSend是用汇编写的。有两个原因:一是因为在C语言中不可能通过写一个函数来保留未知的参数并且跳转到一个任意的函数指针。C语言没有满足做这件事情的必要特性。另一个原因是objc_msgSend必须够快。 当然,谁都不会想要用汇编写下整个复杂的消息查找过程。这没必要。消息发送的代码可以被分为两部分:objc_msgSend中有一个快速路径,是用汇编写的,还有一个慢速的路径,是用C实现的。汇编部分主要实现的是在缓存中查找方法,并且如果找到的话就跳转过去的一个过程。如果在缓存中没有找到方法的实现,就会调用C的代码来处理后续的事情。 因此,objc_msgSend主要有以下几个步骤: 获取传入的对象的类 获取这个类的方法缓存 通过传入的selector,在缓存中查找方法 如果缓存中没有,调用C代码 跳到这个方法的IMP 让我们来看看它是如何完成这些工作的。 逐条指令分析 objc_msgSend根据不同的情况,会有不同的处理路径。它有部分特殊的代码来处理类似发送消息给nil,tagged pointer,以及哈希表碰撞。我会从最常见的正常情况开始讲解:发送消息给一个非nil、非tagged pointer对象,并在消息缓存中找到对应的实现,而不需要额外的扫描等操作的一个过程。 描述完正常情况后,我们将会回来再看一下其他的一些分支情况。 我会罗列每条或者每组指令,并描述它做了些什么,为什么这么做。请注意我将会在罗列出来的指令下面做描述。 每条指令前都会有一个相对函数开始处的偏移量。这可以方便你辨识跳转到哪个目标代码。 ARM64架构下有31个通用寄存器,每个都是64位宽的。他们被标记为x0~x30。同样也有可能使用w0到w30来访问寄存器的低32位。寄存器x0~x7被用于函数入参的前8个参数。这就表示objc_msgSend收到的self参数是保存在x0中,selector _cmd参数在x1里。 开始吧! 0x0000 cmp x0, #0x0 0x0004 b.le 0x6c 这里将存储在x0中的self和0做了一个带符号的比较,如果结果小于等于0,则跳转到0x6c。如果值等于0则说明是nil,所以跳转到的地方就是执行当发送消息给nil的情况。这里也处理了tagged pointer的情况。在ARM64上 通过设置指针的高位来指明是tagged pointer。(x86-64上是设置低位)。如果高位被设置了1,且被作为一个带符号的整型解析的时候,那么值就是负数。一般情况下self是正常的,不会进入这些分支。 0x0008 ldr x13, [x0] 这条指令通过加载x0所指向的内存中的64位,来加载self的isa指针。因为一个对象的第一个指针就是isa指针。此时x13寄存器包含了isa。...
-
iOS Crash Protector
概述 在 iOS 开发中,App的崩溃原因有很多种,这篇文章主要阐述我所使用的防止发送未知消息(unrecognized selector)导致崩溃的方法及思路,希望能起到抛砖引玉的作用。若有错误,欢迎指出! unrecognized selector sent to instance 0x7faa2a132c0 调试过程中如果看到输出这句话,我们马上就能知道某个对象并没有实现向他发送的消息。如果是在已经上线的版本中发现的……GAME OVER…(当然你也可以用热修复) 消息发送的机制我们都明白,通过superclass指针逐级向上查找该消息所对应的方法实现。如果直到根类都没有找到这个方法的实现,运行时会通过补救机制,继续尝试查找方法的实现。那么我们能不能通过重写其中的某个方法,来达到不崩溃的目的? 我们先了解下这个补救机制: 直到最后一步消息无法处理后,我们的App就崩溃了,随后我们就看到了熟悉的unrecognized selector… 这些方法究竟能做什么,我们来看看苹果官方的描述(我对其中比较重要的部分翻译了一下): resolveInstanceMethod: resolveInstanceMethod: 和 resolveClassMethod: 方法允许你为一个给定的 selector 动态的提供方法的实现。 OC 方法在底层的C函数的实现中需要至少两个参数:self 和 _cmd。使用** class_addMethod **函数,你能够添加一个函数到一个类来作为方法使用。 forwardingTargetForSelector: 如果一个对象实现了这个方法,并且返回了一个非空(以及非 self)的结果,返回的对象会用来作为一个新的接收对象,随后消息会被重新派发给这个新对象。(很明显,如果你在这个方法中返回了self,那这段代码将会坠入无限循环。) 如果你这段方法在一个非 root 的类中实现,并且如果这个类根据给定的selector什么都不作返回,那么你应该返回一个 执行父类的实现后返回的结果。 这个方法为对象在开销大的多的 forwardInvocation: 方法接管之前提供了一次转发未知消息的机会。这对你只是想简单的重新定位消息到另一个对象是非常有用的,并且相对普通转发更快一个数量级。如果转发的目的是捕捉到NSInvocation,或者操作参数,亦或者是在转发过程中返回一个值,那这个方法就没有用了。 forwardInvocation: 当对象接受到一条自己不能响应的消息时,运行时会给接收者一次机会来把消息委托给另一个接收者。他委托的消息是通过NSInvocation对象来表示的,然后将这个对象作为** forwardInvocation: 的参数。接收者收到 forwardInvocation: **这条消息后可以选择转发这个NSInvacation对象给其他接收对象。(如果这个接收对象也不能响应这条消息,他也会给一次转发这条消息的机会。) 因此 forwardInvocation: 允许在两个对象之间通过某个消息来建立关系。转发给其他对象的这种行为,从某种意义上来说,他“继承”了他所转发给的对象的一些特征。...