007-objc_msgSend探索(上)

通过这篇文章可以获得什么:

探索原因

在探索类的methodCache的过程中,关注到了cache的读取存储的过程,那么之前cache的insert过程已经探索过了,今天关注一下怎么个读取过程吧,就从objc_msgSend* 开始

cache缓存读取,存储过程

我摘录了objc_cached.mm文件对这个cache过程的描述第62行-第77行,流程如下

  • Cache readers (PC-checked by collecting_in_critical())
  • objc_msgSend*
  • cache_getImp
  • Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
  • cache_t::copyCacheNolock (caller must hold the lock)
  • cache_t::eraseNolock (caller must hold the lock)
  • cache_t::collectNolock (caller must hold the lock)
  • cache_t::insert (acquires lock)
  • cache_t::destroy (acquires lock)

OC的编译时与运行时

在此之前有必要了解一下OC语言的编译时运行时都做了什么:

编译时:

Building,顾名思义就是代码正在编译的时候。那什么是编译,就是机器帮你把源代码翻译成机器能识别的代码,做编译时类型检查(静态类型检查)。在翻译的过程中如果有问题会出现errors或者waring信息,并且帮你做一些提升代码执行效率的优化,这些都是LLVM来做的。

运行时

Running,顾名思义就是程序已经跑起来了,被加载到内存中了,程序被执行了,这是运行时类型检查就与编译时类型检查就不一样了,不在是简单的类型扫描和静态分析,而是在内存中做些操作,做判断……

运行时版本

Objective-C Runtime Programming Guide

  • legacy版本
    • 早期版本,对应Objective-C 1.0,32位Mac OS X的平台上
  • Modern版本
    • 现行版本,对应Objective-C 2.0,iPhone程序和Mac OS X v10.5以后的64位系统中

Runtime API之objc_msgSend探索

由于我要开始对objc_msgSend(Runtime API)进行探索,本质上是发送消息,至于给谁发消息,怎么发消息,不由得就有了几个问题,也是接下来要探索的方向,暂时先列出来:

  1. objc_msgSend它是被谁调用的,为了做什么事情?
  2. 为什么OC代码在编译的时候会被翻译成objc_msgSend,有什么优势?
  3. 是否传递参数,如果有,参数是怎么存储的?
  4. objc_msgSend源码是怎么实现的?为什么选择编,而不是C/C++
  5. objc_msgSendSuper又是什么?

方法的调用方式

日常写代码在Objective-C Code层,很多的Frameworkservices都在这一层,Runtime System Library为底层相关的库,通过complier(编译器层)在中间层进行拦截,向上层Framework以及Runtime提供支持
runtime结构.jpeg

  • 第一种:OC层面方法调用

Objective-C Code.jpeg

  • 第二种:NSObject调用相关的API

Framework&Services.jpeg

  • 第三种:objc提供的下层API

Runtime API.jpeg

方法的调用链路关系:
调用链路关系.jpeg

案例代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //给FFPerson分配内存
        FFPerson *person = [FFPerson alloc];
        //调用方法:OC
        [person likeGirls];
        //调用方法:Framework
        [person performSelector:@selector(likeGirls)];
    }
    return 0;
}
复制代码

编译成.cpp文件

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        FFPerson *person = ((FFPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FFPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("likeGirls"));
        ((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)person, sel_registerName("performSelector:"), sel_registerName("likeGirls"));
    }
    return 0;
}
复制代码

.cpp文件可以感知到,上层的OC代码在底层都会被解释成objc_msgSend,包括alloc等。
FFPerson的alloc方法在.cpp文件中被翻译成了objc_msgSend)((id)objc_getClass("FFPerson"), sel_registerName("alloc"));简化一下,objc_msgSend("FFPerson","alloc"),也就是说,objc_msgSend有两个参数,一个是receiver(消息的接收者),另一个是SEL(方法编号)。但是performSelector这个就不一样了,这次的objc_msgSend参数是三个,分别是receiver、SEL、SEL,也就是说objc_msgSend参数可以大于2个

在代码层玩objc_msgSeng

准备工作:

  1. xcode设置:target -> Build Settings -> App Clang Perprocessing -> Enable Strict Checking of objc_msgSend calls 设置为NO,意思是关闭编译器对objc_msgSend的检查,让你在代码里面可以自由的使用objc_msgSend
  2. 在文件中导入runtime库,即#import <objc/message.h>

案例代码

#import <Foundation/Foundation.h>
#import "FFPerson.h"
#import "FFBoys.h"
#import <objc/message.h>

//OC层方法调用
void methodCall(void) {
    //给FFPerson分配内存
    FFPerson *person = [FFPerson alloc];
    //调用方法:OC
    [person likeGirls];
    //调用方法:Framework
    [person performSelector:@selector(likeGirls)];
}

//objc_msgSend调用
void objc_msgSendCall(void) {
    //给FFperson分配内存
    FFPerson *person = objc_msgSend(objc_getClass("FFPerson"), sel_registerName("alloc"));
    //调用方法
    objc_msgSend(person, sel_registerName("likeGirls"));
}

//objc_msgSendSuper调用
void objc_msgSendSuperCall(void) {
    //给FFperson分配内存
    FFBoys *boys = objc_msgSend(objc_getClass("FFBoys"), sel_registerName("alloc"));
    //创建objc_super对象
    struct objc_super boysSuper;
    boysSuper.receiver = boys;
    //这里的super_class可以是当前类FFBoys或者FFPerson,这里的super_class只是指定方法的第一查找对象,如果当前对象找不到,将会去父类中查找
//    boysSuper.super_class = objc_getClass("FFBoys");
    boysSuper.super_class = objc_getClass("FFPerson");
    //调用super方法
    objc_msgSendSuper(&boysSuper, sel_registerName("likeGirls"));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        methodCall();
        
        objc_msgSendCall();
        
        objc_msgSendSuperCall();
    }
    return 0;
}
复制代码

打印结果

2021-06-25 23:03:20.412797+0800 001-三种方法调用方式[10528:1084950] -[FFPerson likeGirls]
2021-06-25 23:03:20.413186+0800 001-三种方法调用方式[10528:1084950] -[FFPerson likeGirls]
2021-06-25 23:03:20.413404+0800 001-三种方法调用方式[10528:1084950] -[FFPerson likeGirls]
2021-06-25 23:03:20.413534+0800 001-三种方法调用方式[10528:1084950] -[FFPerson likeGirls]
Program ended with exit code: 0
复制代码

我在代码里面定义了三个方法objc_msgSendCallobjc_msgSendCallobjc_msgSendSuperCall,第一个为纯Objective-C方法,第二个为objc_msgSend方法,向FFPerson发送消息,最后一个是objc_msgSendSuper,通过创建了一个FFBoys类继承了FFPerson,声明了一个likeGirls方法,并没有事项,然后通过objc_msgSendSuper向父类中查找方法实现,最终也是可以输出 -[FFPerson likeGirls]的。

结论:

  1. 编译上层代码中间层(C++)会对应一个解释,当法调用时,在中间层对应的是objc_msgSeng消息发送
  2. 上层Objective-C方法的调用在中间层会被翻译成objc_msgSend或者objc_msgSendSuper
  3. objc_msgSend方法构成:objc_msgSend(消息的接收者消息主体(sel+参数))
  4. super_class设置的是receiver,设置为,调用方法时候第一响应者就是谁。
  5. 通过这个探索过程,方法调用的本质是消息发送

objc_msgSend汇编分析

objc_msgSend汇编根据sel查找imp的流程

  • 第一步:cmp p0, #0 p0为消息接收者receiver,比较p0与0,如果没有接收者,则此次objc_msgSend没有意义
  • 第二步:判断是不是SUPPORT_TAGGED_POINTERS类型,意思是不是tagged pointers指针如果是,执行b.le LNilOrTagged,然后在里面执行 b.eq LReturnZero。如果不是SUPPORT_TAGGED_POINTERS类型,直接b.eq LReturnZero,此次objc_msgSend无效,结束本次消息发送。
  • 第三步:如果p0存在,则将x0存入到p13,x0是receiver,即,即类的首地址,即isa,也就是说p13=isa
  • 第四步:进入GetClassFromIsa_p16,传递的参数src=p13needs_auth=1auth_address=x0,判断是不是SUPPORT_INDEXED_ISA(32位isa),不满足此条件,接下来会进入__LP64__(这份源码里指的是Mac OS X)分支。
  • 第五步:由于_need_auth=1,进入分支ExtractISA p16, \src, \auth_address,此ExtractISA为,操作是将\src(isa)#ISA_MASK操作,得到了Class,结果存入到p16中。
  • 第六步LGetIsaDone:获取isa完成。接下来执行CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached:
  1. mov x15, x16,隐藏isa,将x16寄存器赋值到x15
  1. 现在探索的是arm64架构的汇编,所以进如分支CACHE_MASK_STORAGE_HIGH_16,执行ldr p11, [x16, #CACHE]指令,#define CACHE 8,那么p11=x16+0x8,等同于isa+0x8,即isa向右偏移8字节,拿到了cache_t,即p11=cache_t
  1. 进入到CONFIG_USE_PREOPT_CACHES分支,我这里分析非A12Z即以上芯片,所以不进入#if __has_feature(ptrauth_calls)分支,进入else执行and p10, p11, #0x0000fffffffffffe指令,将p11#0x0000fffffffffffe(preoptBucketsMask)得到buckets地址,存在p10。然后执行指令tbnz p11, #0,作用是验证p11,也就是cache_t是不是为0,如果为0,则证明没有缓存,没有向下继续查找buckets的必要了,跳转至LLookupPreopt
  1. eor p12, p1, p1, LSR #7,因为p0寄存器是receiverp1寄存器为第二个参数SEL _cmd,所以p1=_cmd,对应上面的指令就可以得出p12 = (_cmd >> 7) ^ _cmd
  1. and p12, p12, p11, LSR #48,p11 = chahe_t = _bucketsAndMaybeMask,可以翻译成p12 = p12 & (_bucketsAndMaybeMask >> 48),这个指令最终结果是找到已知buckets的index。
  1. add p13, p10, p12, LSL #(1+PTRSHIFT),PTRSHIFT在__LP64__下的值为3,否则为2,我这里探究的64位的,所以PTRSHIFT=3,p10是buckets,p12是index,那么可以将上述指令翻译为:p13 = p10 + (p12 << (1+3)),将index左移4位,然后将得到结果n,在buckets的首地址上移动响应n个步长,找到最终的bucket_t.
  1. 1: ldp p17, p9, [x13],将x13寄存器的值取出来放在p17和p9,因为x13为bucket_tj结构体,在arm64架构下,第一个值是imp,第二个值是sel,所以p17=imp,p9=sel。
  1. cmp p9, p1,比较p1与p9,即比较我在缓存中取到的sel和objc_msgSend的第二个参数_cmd,如果不等于向后跳转,执行指令b.ne 3f,将执行3条指令:
  • 第一条cbz p9, \MissLabelDynamic,意思是找不到sel
  • 第二条cmp p13, p10循环查找的条件,当要查找的bucket_t的地址大于bucets的首地址的时候,继续查找
  • 第三条b.hs 1b,重新回到1执行sel的比较,如果相等2: CacheHit即命中了缓存中的方法,找到了缓存,进入到CacheHit
  • 第七步 CacheHit分为3种模式,#define NORMAL 0,#define GETIMP 1,#define LOOKUP 2,无论哪种,最终结果都是将去查找sel对应的imp,然后将其返回

源码

_objc_msgSend

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	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
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret
复制代码

GetClassFromIsa_p16

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, \src			// 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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
复制代码

ExtractISA

.macro ExtractISA
	and	$0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
	xpacd	$0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
	mov	x10, $2
	movk	x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
	autda	$0, x10
#endif
.endmacro
复制代码

CacheLookup

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
	and	p12, p1, p11			// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

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

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
复制代码

CacheHit

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

复制代码

objc_msgSend汇编流程图

objc_msgSend汇编流程图.png

底层汇编探索感悟

在objc_msgSend汇编的探索过程中,发现其实本质代码并不多,逻辑也很简单,相对于上层代码,汇编就很直接,难点在于不知道一些指令的含义,另一些伙伴望而却步。其实完全没有必要,晚一些时间,下一些功夫,就是没有基础的伙伴也是可以看懂的,看汇编最重要的一点一定要平心静气,不能急,不能急,不能急。重要的事情说三遍。原所有在汇编遨游的伙伴都能如鱼得水。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享