Swift教程

iOS objc_msgSend 汇编分析

本文主要是介绍iOS objc_msgSend 汇编分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

概述

Objective-C 里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由 selector、函数指针imp 和 metadata 组成的。objc_msgSend的工作就是传入对象和selector,查找相应方法的函数指针,然后跳到函数指针所指向的位置。

objc_msgSend是用汇编写的原因有两个:

  • 在C语言中不可能通过写一个函数来保留未知的参数并且跳转到一个任意的函数指针。
  • objc_msgSend 的调用频次最高,几乎所有方法的调用都要经过它,因此要够快。

消息发送的代码可以被分为两部分:objc_msgSend中有一个快速路径,是用汇编写的,还有一个慢速的路径,是用C实现的。汇编部分主要实现的是在缓存中查找方法,并且如果找到的话就跳转过去的一个过程。如果在缓存中没有找到方法的实现,就会调用C的代码来处理后续的事情。

分析objc_msgSend的流程:

  1. 获取传入的对象的类
  2. 获取这个类的方法缓存
  3. 通过传入的selector,在缓存中查找方法
  4. 如果缓存中没有,调用C代码
  5. 跳到这个方法的IMP

根据上述流程分析objc_msgSen的汇编。

现在,苹果公司已经开源了 Objective-C 的运行时代码。你可以在苹果公司的开源网站,找到 objc_msgSend 的源码。

指令分析

ARM64架构下有31个通用寄存器,每个都是64位宽的。他们被标记为x0~x30。同样也有可能使用w0到w30来访问寄存器的低32位。寄存器x0~x7被用于函数入参的前8个参数。这就表示objc_msgSend收到的self参数是保存在x0中,selector _cmd参数在x1里。

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
复制代码

判断存储在p0中的self是否为空。如果小于0,跳转到 LNilOrTagged, 执行tagged_pointers情况,如果等于0,跳转到 LReturnZero ,执行发送消息给nil的情况。(在ARM64上 通过设置指针的高位来指明是tagged pointer。(x86-64上是设置低位)。如果高位被设置了1,且被作为一个带符号的整型解析的时候,那么值就是负数。一般情况下self是正常的,不会进入这些分支。)

ldr	p13, [x0]		// p13 = isa
复制代码

加载x0所指向的内存,实则是加载self的isa指针。一个对象的第一个指针就是isa指针。p13寄存器存储了isa。

GetClassFromIsa_p16 p13		// p16 = class
复制代码

执行宏 GetClassFromIsa_p16

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, $0			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
	// 64-bit packed isa
	and	p16, $0, #ISA_MASK

#else
	// 32-bit raw isa
	mov	p16, $0

#endif

.endmacro
复制代码

判断 SUPPORT_INDEXED_ISA ,表示isa_t中存放的信息是Class的地址,还是一个索引(根据索引可以在类信息表中查找该类的结构地址)。iOS设备SUPPORT_INDEXED_ISA 为0,然后判断是否为64位,执行

and	p16, $0, #ISA_MASK
复制代码

ARM64可以使用非指针的isa。通常isa指针指向的是对象的类,但是非指针的isa利用了备用的bit位,填充了一些其他的信息。这条汇编指令执行了一个逻辑与运算,掩盖掉了所有额外的位,把实际的指向类的指针保存在p16寄存器中。

.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	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
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// 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
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
复制代码

分析上面的宏

#define CACHE            (2 * __SIZEOF_POINTER__)

ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
复制代码

从x16寄存器编译16个字节,取到的值保存到p10和p11中。方法缓存的结构如下:

typedef uint32_t mask_t;
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
复制代码

p10存储了buckets的值,p11的高32位保存了_occupied,低32位保存了_mask。

_occupied指定了哈希表中包含了多少条目,在objc_msgSend中不起什么作用。_mask很重要:它描述了哈希表的尺寸,方便用于与运算的掩码。它的值总是一个2的幂减一,用二进制的方法描述看起来就像是000000001111111,末尾是可变数量的1。通过这个值可以知道selector的查找索引,并在查找表的时候包裹着结尾。

and	w12, w1, w11		// x12 = _cmd & mask
复制代码

x1中包含_cmd,所以w1包含了_cmd的低32位。w11包含了上面提到的_mask。这条指令将这两个值做与运算并将结果放到w12中。结果相当于是计算_cmd & mask,但是避免了开销很大的模运算。这一步计算出了传入的selector的起始哈希表的索引。

add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
复制代码

这条指令通过索引表的指针向左位移,再加上buckets,得到第一个查找的bucket的地址,也就是imp。

ldp	p17, p9, [x12]		// {imp, sel} = *bucket
复制代码

把x12保存的bucket的地址,每个bucket包含一个selector和imp。p17包含了当前的imp,p9包含了当前的selector。

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
复制代码

对p1中的cmd和p9中的selector比较。如果相等,CacheHit将imp返回。接下去就是执行目标方法的代码了,objc_msgSend的快速路径到此已经结束了。所有参数寄存器不会受到干扰,原封不动的传给目标方法,就好像直接调用了目标方法一样。如果不相等,跳转到2f位置,处理不相等的逻辑,执行CheckMiss。

来看CheckMiss的宏定义

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
复制代码

由于传入的是nomal模式,会进入__objc_msgSend_uncached

cbz	p9, __objc_msgSend_uncached
复制代码

p9包含了从bucket加载的selector,这条指令是p9和0作比较,如果等于0跳转到__objc_msgSend_uncached,这就说明这是个空的bucket,意味着目标方法不在缓存中,这时候会进入C语言方法__objc_msgSend_uncached,执行详细的查找流程。如果不为0就说明bucket不是空,只是没有找到,则继续查找。

cmp	p12, p10		// wrap if bucket == buckets
b.eq	3f
复制代码

p12中存储的是当前的bucket地址,p10中存储的是buckets哈希表的首地址,比较如果匹配,跳转到3f处。 如果不匹配会继续执行

ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop
复制代码

再一次从缓存的bucket中加载。这次他从偏移量为BUCKET_SIZE的地方加载当前缓存bucket的地址。地址引用末尾的感叹号是一个有趣的特性。这指定一个寄存器进行回写,意思就是寄存器会更新为计算后的值。这条指令有效的执行了x12 -= 16来加载新的bucket,并使x12指向这个新的bucket。

现在已经加载了一个新的bucket,所以接下去的执行就要回到之前的检查当前bucket是否匹配的代码。这条指令代表回到1b,使用新的值再执行一次所有代码。如果仍然没有找到匹配的bucket,这些代码会持续执行,直到找到匹配的,或者空的bucket,或者命中表的开头。

3f处匹配的逻辑指令:

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// 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
复制代码

x12中包含了当前的bucket指针,w11表示的是mask,表的大小。将mask左移1+PTRSHIFT位,加上buckets的首地址。得到的结果指向表的末尾。

把得到的新的bucket存储到p17,p9。

cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
复制代码

这段代码还是去检测bucket是否匹配,并且跳转返回imp。我们可以看到有两次123的流程,第二次123就是为了防止两种情况:

第一种,多线程调用的时候给的一次容错机会。

第二种是 为了在遇到内存被破坏或者无效对象时,防止陷入无限循环而榨干性能。举个例子,堆损坏能够在缓存中塞满非0的数据,或者设置缓存的掩码为0,缓存不命中就会一直循环执行缓存扫描。额外的检查可以停止循环,将问题转变为崩溃日志。

参考资料
[Dissecting objc_msgSend on ARM64](https://www.mikeash.com/pyblog/friday-qa-2017-06-30-dissecting-objc_msgsend-on-arm64.html)
[剖析ARM64下的objc_msgSend](http://madmark.cc/2017/08/01/ARM64_objc-msgSend/)复制代码
这篇关于iOS objc_msgSend 汇编分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!