手撕iOS底层13 -- 手摸手的助你理清`objc_msgSend`汇编源码

2,457 阅读8分钟

有没有内推,最近想换个坑位。

0x00 - 运行时简介

  • Runtime

运行时,代码跑起来,已经加载到内存中, 只有加入到内存中, 代码才被激活,才跑起来

  • Buildtime

代码在未加载到内存中之前是死家伙,就存在磁盘上

Objective-C Runtime Programming Guide 官方文档

一套API, c/c++/汇编,为oc提供运行时的功能;

  • 早期版本对应的编程接口:1.0

  • 现行版本对应的编程接口:2.0

早期版本用于Objective-C 1.0 , 32位的Mac OS X的平台上

现行 版本: iPhone 程序和Mac OS X v10.5及以后的系统中64位程序

@selector() == sel_registerName == NSSelectorFromString() 不同层面调用方式不一样,结果一样

使用runtime的三种方式

  • 通过Objective-C代码调用 [obj someThing]

  • 通过NSObject接口调用 NSSelectorFromString() isKindOfClass()等

  • 通过Runtime APIs函数调用 sel_reisterName, class_getInstanceSize等函数

0x01 - 方法的本质探索

发送消息

objc_msgSend(id , sel)

id 消息的接受者 sel 方法的编号

objc_superMsgSend 向父类发送消息

手撕iOS底层08 -- isa走位详解中,通过clang编译,clang -rewrite-objc main.m -o main.cpp

通过打开main.cpp分析,理解了OC对象的本质,同样的在这个文件中,

// main函数中oc方法的调用
Person *p = [Person alloc];
[p say1];
[p say2];
// clang 编译后底层实现
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say1"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("say2"));

通过这段代码对比,得到方法的本质就是objc_msgSend消息发送, 可以在main函数中直接使用objc_msgSend来调用say1这个函数。

Person *p = [Person alloc];
objc_msgSend(p, @selector(say1));
[p say1];
// -[Person say1:]
// -[Person say1:]

⚠️ 这里需要设置 Build Settings -> Enable Strict Checking of objc_msgSend Calls -> NO 关闭这个静态检测函数参数问题

最终输出的结果一样的。

objc_msgSendSuper子类对象调用父类的方法
@interface Person : NSObject
  -(void)sayBye;
@end
  
@implementation Person
  - (void)sayBye {
  NSLog(@"bye bye");
}
@end
  
@interface Teacher : Person
@end
  
@implementation Teacher
@end
  
  // 在 main 中执行
Person *p = [Person alloc];
Teacher *t = [Teacher alloc];
[t say2];
struct objc_super mySuper;
mySuper.receiver = t;
mySuper.super_class = [Person class];
objc_msgSendSuper(&mySuper, @selector(say2));

给父类发送消息需要传递一个struct objc_super的结构体指针

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

结果都是输出

LGPerson say : -[Person say2]
LGPerson say : -[Person say2]

通过输出结果,发现[t say2]objc_msgSendSuper都是执行父类say2方法实现。所以这里暂时得到一个结论方法调用,先在自己所属的类中查找,如果自己的类没有,会走继承链查找

0x02 - 简要分析objc_msgSend流程

通过在objc-781源码中搜索 objc_msgSend,由于我们的目标设备是arm64架构,所以我们打开objc-msg-arm64.s汇编文件来查看。

这个汇编的整体流程是:

  1. 从消息接受这查找方法, 先是通过对象isa找到类
  2. 通过类去cache_t的缓存查找方法
  3. 缓存没有,去bitsmethodList查找方法

objc_msgSend本身使用汇编语言写的,为什么使用汇编?原因主要有俩个:

  • OC语言中调用方法,都是通过objc_msgSend ,可以说这个方法是OC方法必经之路,所以在这个方法上面进行性能优化能够提升整个App生命周期的性能, 而汇编语言在性能上优化是属于原子级优化,能够做到极致。

  • 其它语言难以实现未知参数跳转任意函数指针的功能

  • 快 效率高 + 动态性(不确定性) 参数不确定

id objc_msgSend(id self, SEL _cmd, ...);

现获取对象对应类的信息,再获取方法的缓存,根据selector查找函数和指针,经过异常处理后,最后跳到对应的函数实现

0x03 - arm64 objc_msgSend汇编分析

ENTRY _objc_msgSend //入口
UNWIND _objc_msgSend, NoFrame // 没有窗口界面

cmp p0, #0 //nil check and tagged pointer check 检查第一个参数,即self是否为0

#if SUPPORT_TAGGED_POINTERS // 如果支持SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative) 如果小于等于0  就跳转到LNilOrTagged处理
#else // 不支持 SUPPORT_TAGGED_POINTERS
	b.eq	LReturnZero // 是否等于0 等于0就跳转到 LReturnZero 进行nil处理
#endif
	ldr	p13, [x0]		// p13 = isa //
	GetClassFromIsa_p16 p13		// p16 = class

ldrLoad Register 的缩写,[]是间接寻址, 表示从x0所表示的地址中取出8字节数据,放到x13中,x0self的地址,所以这里拿到的是isa的地址, 这里解释一下

因为self是指针, 实际上是指向struct objc_object, 定义是


struct objc_object {
    private:
  	isa_t isa;
  ....
}

objc_object中只有一个成员isa,因此取出指针指向的内容,也就是取到了isa的值。

调用GetClassFromIsa_p16 进一步获取class的地址,这里是重点, 因为后续都要使用这个class

获取class

GetClassFromIsa_p16 的实现

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA // 这部分主要是watchOS上支持
	// 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__ //arm64
	// 64-bit packed isa
	and	p16, $0, #ISA_MASK //

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

#endif

.endmacro

and p16, $0, #ISA_MASK

and表示与运算, p16=$0 & ISA_MASK, $0是第一个传进来的参数, 也就是isa的值,ISA_MASK 0x0000000ffffffff8ULL

这个和objc-781源码

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK); // 这里也是用与的算法来获取class
#endif
}

到这里我们就获取到了class的地址,就可以跳到LGetIsaDone:,进行缓存的查找CacheLookup

由于对象的实例方法存储在所属的类中,那么它的方法缓存也在类里面,简单回顾下上一篇文章的内容

struct objc_class : objc_object {
    // Class ISA;       // 8
    Class superclass;   // 8
    cache_t cache;      // 16        // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 属性 方法 协议 //8
}

可以看我上一篇文章了解cache_t里的缓存知识,现在的寄存器状态是 x1 = SEL, x16 = isa

查找过程

CacheLookup NORMAL|GETIMP|LOOKUP <function>

// p1 = SEL, p16 = isa
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

CACHE定义如下,2个指针大小,也就是16字节

#define CACHE (2 * __SIZEOF_POINTER__)

这段是说,从x16寄存器中偏移CACHE个位置取出8个字节大小的数据,放入到p11中,

为什么偏移16字节, 看objc_class的定义


struct objc_class : objc_object {

  // Class ISA; //8字节
 
 Class superclass;  // 8字节

 cache_t cache; // formerly cache pointer and vtable

 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

...

}

cache所在的位置在ISAsuperclass后边,所以要偏移16字节,再取出8字节的内容,就得到了_maskAndBuckets值,

	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask

之前的文章提到过_maskAndBuckets buckets存在低48位,

p10=p11&0x0000ffffffffffff 得到p10=buckets

LSR表示逻辑右移, p11右移48位, 得到maks放到p11p1里放的是sel,通过sel&mask得出sel在哈希表中的index

// 得到mask值
p11 = p11 >> 48
// 求出index
p12 = p1 & p11

接着获取索引项的对应地址

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

LSL逻辑左移, PTRSHIFT=3, 就是p12左移4位, 相当于乘以16, 因为每一个bucket大小是16字节, 所以index也就是索引是多少, 偏移多少大小,p10保存的是buckets缓存表的地址,加上偏移量,就得到索引项的地址,存入p12中。

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket

通过ldr指令 从x12中的地址中取出2个8字节的数据,放到p17p9中, p17imp数据, p9sel,从bucket_t的数据结构中得知,缓存表中的每一项都是{imp, sel}

这里开始俩次扫描缓存表,如示意图

1:	cmp	p9, p1			// if (bucket->sel != _cmd) 这里是比较参数的sel(_cmd)和缓存的sel
		b.ne	2f			//     scan more 不想等, 跳转到2执行
		CacheHit $0			// call or return imp 命中缓存 , 调用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

通过CheckMiss $0检查当前indexbucket是否为空,

.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

ChickMiss中通过p9判断是否为0, p9表示当前sel,如果为空,就执行__objc_msgSend_uncached,会进行c方法的慢速查找流程。

不为空就会走后边的流程

cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f

p10是当前缓存表的地址,p12是当前selbucket,意思判断当前的p12是否是当前的表头,如不是就不断的循环比较

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

ldp后边的指令跟了一个!,表示将p12减去一个BUCKET_SIZE大小,写回p12中,再取出IMPSEL分别给p17p9中。

如果是表头,会走3处理

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)

就是将p12指向表尾,然后再从后向前遍历方法缓存,p11是当前缓存的地址,因为mask再其中占高16位,所以右移48位,得到mask,因为每一项bucket大小是16字节,所以要mask << 4得到总共占用的大小,也就是总的偏移量,再用p12首地址去加上总的偏移量,所以p12就指向表尾了

// 分步表示
mask = p11 >> 48
offset = mask << 4

然后开始[第二次]遍历方法缓存,从新走 第一步和第二步的流程,第三步会跳出来

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0


整体代码参考

	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
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// 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

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

未找到方法最后都会跳到__objc_msgSend_uncached

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
	
	.macro MethodTableLookup
	// 保存寄存器 start
	// push frame 
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]
	// 保存寄存器 end
	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward //调用lookUpImpOrForward查找

	// IMP in x0 
	mov	x17, x0 //由于返回结果是放在 x0 中,之前缓存查找结果的 imp 是放在 x17 中,这里保持一致
	
	// 恢复寄存器
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro

.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0 //只是调用br指令 跳转传入的IMP
.endmacro

到这里就结束了, 后续再补充Tagged pointernil的处理。


参考链接


欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步;