objc_msgSend源码底层探索

657 阅读6分钟

前言

上一篇文章类的底层探究-cache中提到cache_t插入bucket调用的是cache_t::insert方法,全局搜索我们看到下面的注释: image.png 从注释中看出写入cache的方法有copyCacheNolockeraseNolockcollectNolockinsertdestroy 我们好奇的是cache的读取为什么有objc_msgSendcache_getImp,本文主要研究objc_msgSend,针对方法的本质,objc_msgSend的底层源码,以及objc_msgSend的真机调试进行探究

方法的本质

运行时runtime的调用方法

  • OC代码直接的方法调用,比如[person run]
  • NSObject系统库,接口调用,比如isKindOfClass
  • runtime API, 比如objc_msgSendobjc_msgSendSuper

前面两种方式比较简单,采用第三种方式调用的时候需要注意的是引入objc/message.h,以及禁止objc_msgSend调用的严格检查 image.png

下面是模拟器的调用,可以看到已经能成功调用objc_msgSendimage.png

但是使用真机运行后报错了: image.png

所以可以得到objc_msgSend的调用跟指令集有关的结论。 在arm指令集下,我们采用Apple官方建议的方式调用api: image.png 可以正常调用了😄

方法的本质

对于代码

SPObject *obj = [[SPObject alloc] init];
[obj runSP];

我们xcrun一下(clang也一样)得到cpp文件,查看相关代码

  SPObject *obj = ((SPObject *(*)(id, SEL))(void *)objc_msgSend)((id)((SPObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("SPObject"), sel_registerName("alloc")), sel_registerName("init"));
  ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("runSP"));

化简一下就是objc_msgSend(obj,sel_registerName("runSP") 那么对于有参数的方法调用是怎么样的呢

((void (*)(id, SEL, id))(void *)objc_msgSend)((id)obj, sel_registerName("runSP2:"), (id)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2));

化简一下就是objc_msgSend(obj,sel_registerName("runSP2:"), @2) 这里我们可以得出结论,方法的本质就是objc_msgSend

objc_msgSend源码分析

我们通过跟进汇编流程,发现objc_msgSend的调用是在libobjc.A.dylib里面: image.png 同时在libobjc里面也找到了objc_msgSend的声明: image.png

找到了声明,但是点击定位不到方法的实现。我们想要是objc_msgSend方法的实现是c或者是c++方法应该直接可以定位,于是我们大胆猜想objc_msgSend方法的实现是汇编的方法,汇编的方法需要前面加下划线(为了防止符号冲突)全局搜_objc_msgSend image.png

我们看到有好多文件用到了_objc_msgSend这个符号,且都是汇编代码,不同的文件区分不同的架构,这里笔者选择arm64架构的代码进行探索

_objc_msgSend汇编中用到的一些宏定义说明 MSG_ENTRY, ENTRY, STATIC_ENTRY, UNWIND

.macro MSG_ENTRY /*name*/
// 伪操作 它不是真正的指令
// 定义代码段 只读和可执行的 后面那些指令都属于.text段
	.text
// .align提供的参数作为对齐目标 圆整对象为 2^10
// 作用在于对指令或者数据的存放地址进行对齐,
// 有些CPU架构要求固定的指令长度并且存放地址相对于2的幂指数圆整,否则程序无法正常 运行,比如ARM
	.align 10
//告诉汇编器 $0(也就是_objc_msgSend的函数地址)要被链接器用到
	.globl    $0
$0:
.endmacro
.macro ENTRY /* name */
// 定义代码段 只读和可执行的 后面那些指令都属于.text段
  .text
// .align提供的参数作为对齐目标 圆整对象为 2^5
  .align 5
//告诉汇编器 $0(也就是_objc_msgSend的函数地址)要被链接器用到
  .globl    $0
$0:
.endmacro
.macro STATIC_ENTRY /*name*/
  .text
  .align 5
//private_extern可以把symbol_name变成私有外部符号。
//当链接编辑器将此模块与其他模块组合在一起
//(且keep_private_externs 命令行选项未指定)时,该符号会将它从全局更改为静态
  .private_extern $0
$0:
.endmacro
.macro END_ENTRY /* name */
// 定义方法出口
LExit$0:
.endmacro
.macro UNWIND
//定义section端
  .section __LD,__compact_unwind,regular,debug
  PTR $0
// 计算函数占的空间大小
  .set  LUnwind$0, LExit$0 - $0
// 分配函数空间
  .long LUnwind$0
// 分配NoFrame或者FrameWithNoSaves的空间大小
  .long $1
// 非私有的
  PTR 0  /* no personality */
// LLVM对于LSDA的解释,不明觉厉
// Language Specific Data Area. C++ “zero cost” unwinding is built on top a generic unwinding mechanism.
// As the unwinder walks each frame, it calls a “personality” function to do language specific analysis.
// Each function’s FDE points to an optional LSDA which is passed to the personality function.
// For C++, the LSDA contain info about the type and location of catch statements in that function.
  PTR 0  /* no LSDA */
// 定义代码段 后面要开始执行代码了
  .text
.endmacro
#define NoFrame 0x02000000  // no frame, no SP adjustment 
#define FrameWithNoSaves 0x04000000  // frame, no non-volatile saves

_objc_msgSend源码分析

//定义方法入口
  MSG_ENTRY _objc_msgSend
//准备section段,分配方法空间,开始执行text段的代码
  UNWIND _objc_msgSend, NoFrame

//p0=x0寄存器,也就是_objc_msgSend的第一个参数self
//p1=x0寄存器,也就是_objc_msgSend的第二个参数sel
//我们假定是对象发送消息,p0就是消息的接受者
//比较p0是否是nil
  cmp p0, #0      // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//支持小对象模式的话小于0就是空对象或者小对象,走LNilOrTagged流程
  b.le  LNilOrTagged    //  (MSB tagged pointer looks negative)
#else
//不支持小对象模式的话对象为空,走LReturnZero流程
  b.eq  LReturnZero
#endif

//这里的流程是对象不为空的流程
//对象的指针指向的是对象的isa, p14=对象的isa
  ldr p14, [x0]   // p14 = raw isa
//对象的isa指向类,这个方法执行完后 p16=类
  GetClassFromIsa_p16 p14, 1, x0  // p16 = class
LGetIsaDone:
// 这里开始查找类里面的缓存的cache_t里面的方法获取imp
// 根据传入的模式返回imp或者调用imp或者走__objc_msgSend_uncached缓存未命中的流程
// calls imp or objc_msgSend_uncached
  CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//TaggedPionter流程
//0的话是空对象走,LReturnZero流程
  b.eq  LReturnZero   // nil check
//不然就是小于0的小对象走GetTaggedClass获取小对象
//GetTaggedClass流程暂时不分析,看最后的返回应该也是拿到类存到p16寄存器
  GetTaggedClass
//也是走LGetIsaDon流程去类的cache_t里面找imp
  b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
//p0是nil才回来这个流程
//这方法把参数x0,x1和返回值d0,d1,d2,d3这几个寄存器置0,然后返回
  // x0 is already zero
  mov x1, #0
  movi  d0, #0
  movi  d1, #0
  movi  d2, #0
  movi  d3, #0
  ret

  END_ENTRY _objc_msgSend

p0=x0的原因是有相关宏定义,这里定义了17个寄存器的别名

image.png

分析_objc_msgSend整个流程大概是这样的:

1:对于方法接受者p0判断是否为空,空的话走LReturnZero流程,把参数和返回值置0,然后返回

2:如果p0不为空,判断是否是小对象,是的话走GetTaggedClass获取小对象的类存在p16

3:不是小对象的话,获取对象的isa存到p14

4:根据对象的isa获取类,存到p16

5:根据类查找cache,调用CacheLookup,找到imp返回或者调用,没找到走__objc_msgSend_uncached流程去类的bitsmethods里面找,或者进行消息动态决议或转发

GetClassFromIsa_p16

接下来我们再分析里面的GetClassFromIsa_p16流程,探究怎么从对象的isa获取类的:

// 如果是个Indexed isa流程是跟iwatch相关的,暂时不做探究
#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:

//不是Indexed isa并且是64位指令的话
#elif __LP64__
//needs_auth=0说明是纯isa,src指向的就是类
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    mov p16, \src
.else
//64位非纯isa的话需要通过auth_address(isa_mask)与上isa获取类
    // 64-bit packed isa
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov p16, \src

#endif

.endmacro

其中SUPPORT_INDEXED_ISA的说明: image.png __ARM_ARCH_7K__是个啥东西,源码里找不到定义,我们通过clang看出了一些端倪

// Unfortunately, __ARM_ARCH_7K__ is now more of an ABI descriptor. The CPU
// happens to be Cortex-A7 though, so it should still get __ARM_ARCH_7A__.
if (getTriple().isWatchABI())
    Builder.defineMacro("__ARM_ARCH_7K__", "2");

综上看SUPPORT_INDEXED_ISAiwatch的判断 对于ExtractISA的定义:

#if __has_feature(ptrauth_calls)(iponex以上的设备)的条件下会走: image.png 非iphonex的会走:

image.png

这里的$0=p16$1=src=p14=isa, ISA_MASK就是isa的掩码isa与上isa掩码获取类,p16=

CacheLookup的流程

//Mode有GETIMP,NORMAL,LOOKUP,_objc_msgSend传入的是NORMAL
//Function是_objc_msgSend
//MissLabelDynamic=__objc_msgSend_uncached
//MissLabelConstant只有Mode=GETIMP才会用到,这里不做探究
.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
//#define CACHE            (2 * __SIZEOF_POINTER__)
//类内存平移16个字节得到cache_t的指针地址
//cache_t的内存付给p11,p11=_bucketsAndMaybeMask
//p11的64位在真机下后48位存buckets,前16位存mask
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets


#if CONFIG_USE_PREOPT_CACHES

#if __has_feature(ptrauth_calls)
//高16位抹0,得到低48位的buckets给到p10
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif

//p1是_objc_mesSend的第二个参数sel
//这里面是执行cache_hash函数通过mask和sel获取sel对应的初始化下标p12
	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


//获取初始下标的bucket,下标p12左移4位等于乘以16(16是因为一个bucket包含8字节IMP合8字节的SEL)
// p13=初始的bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {

// #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__)
// BUCKET_SIZE是16个字节,也就是一个bucket的内存大小
// cache_next函数往前遍历第一遍循环找到buckets的首地址
// p9=SEL p17=IMP
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
//p17和参数p1对比,相等的话找到了,不然就继续走3的分支
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//如果sel==0说明没找到,走MissLabelDynamic流程
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
//从初始下标往前遍历找知道buckets的头部位置
	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
//获取buckets的尾地址
	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

//mask是capacity-1,到这边p13正好是buckets中最后一个bucket
//p12是初始下标,p10是buckets,经过这个操作后p12 =buckets的第一个bucket
	add	p12, p10, p12, LSL #(1+PTRSHIFT)

//从buckets尾地址开始继续循环往前找知道找到p12这个初始位置,循环终止,
//这样前面部分和后面部分的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
        

分析对于真机整个流程大概是这样的:

  • 1.类内存平移16个字节得到cache_t的指针地址
  • 2.根据架构从_bucketsAndMaybeMask(_mask)获取buckets以及mask
  • 3.根据参数sel进行hash函数,算出初始的下标
  • 4.根据初始下标跟buckets首地址算出初始的bucket
  • 5.往前遍历bucket直到buckets的首地址
  • 6.从buckets的最后一个元素往前遍历直到初始的bucket
  • 7.循环过程中如果缓存命中走CacheHit流程
  • 8.如果没找到sel走MissLabelDynamic流程

缓存命中CacheHit流程

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
// TailCallCachedImp是认证IMP和调用IMP的过程,objc_msgSend找到IMP就调用了
// TailCallCachedImp根据是否是iphoneX以上的机型有不同的定义
	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

#if __has_feature(ptrauth_calls) //iphoneX以上
.macro TailCallCachedImp IMP, IMPAddress, SEL, ISA
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	\IMPAddress, \IMPAddress, \SEL	// mix SEL into ptrauth modifier
	eor	\IMPAddress, \IMPAddress, \ISA  // mix isa into ptrauth modifier
.ifndef LTailCallCachedImpIndirectBranch
LTailCallCachedImpIndirectBranch:
.endif
	brab	\IMP, \IMPAddress
.endmacro

//其他机型
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
.ifndef LTailCallCachedImpIndirectBranch
LTailCallCachedImpIndirectBranch:
.endif
	br	$0
.endmacro

缓存命中的代码比较简单,对于MODE=NORMAL来说是认证IMP调用 IMP的过程

缓存命中MissLabelDynamic流程

CacheLookup的参数得知MissLabelDynamic=__objc_msgSend_uncached,定位__objc_msgSend_uncached方法

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search

//快速查找没找到调用_lookUpImpOrForward进行慢速查找,并且循环继承链查找
//还是没找到就动态决议,消息快速转发和慢速转发流程
MethodTableLookup
//TailCallFunctionPointer就是跳转到x17
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup

SAVE_REGS MSGSEND

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
// x0和x1本身就是objc_msgSend的参数就不用处理了
// x2是cls,之前的流程已经存在了p16了
// x3是参数behavior,这里写死是3
//其中LOOKUP_RESOLVER=2,用来控制动态决议走一次resolveMethod_locked
mov	x2, x16
mov	x3, #3
bl	_lookUpImpOrForward
// IMP in x0
mov	x17, x0

RESTORE_REGS MSGSEND

.endmacro

缓存没命中配置好_lookUpImpOrForward的参数然后调用_lookUpImpOrForward方法,返回的结果在x0,赋值给x17,然后TailCallFunctionPointer x17,直接调用方法

真机探索_objc_msgSend流程

随便对一个OC方法加断点打开汇编,看到结果如下:

image.png

image.png

image.png

image.png 到这里我们进入了objc_msgSend的真机汇编流程

image.png 这里看到x0是消息接受者,x1sel

image.png 这里x14=对象的isa,x16=

image.png

image.png 这里p11 = mask|bucketsp10 = buckets

image.png 这里获取初始下标 p12以及初始bucket=p13,我们看到这里已经存了IMPSEL了,但不是我们要找的,继续循环找

image.png

image.png

image.png

image.png 笔者尝试的方法是没有实现的,所以几次循环后最终会走缓存没命中的流程,直到crash。读者可以根据汇编源码的不同分支尝试探索不同的案例,比如缓存命中 TaggedPionter模拟器iWatch等case进行探索,思路是一样的,这里不再赘述

总结

  • cache_t::insert是写入cache,cache的读取在objc_msgSendcache_getImp
  • 运行时runtime的调用方法
    • OC代码直接的方法调用
    • NSObject系统库,接口调
    • runtime API
  • 方法的本质就是objc_msgSend
  • _objc_msgSend源码流程:
    • 方法接受者p0判断是否为空,空的话走LReturnZero流程
    • 不是的话判断是否是小对象,是的话走GetTaggedClass流程
    • 不是的话根据对象的isa获取类,存到p16
    • 根据类调用CacheLookup
  • CacheLookup流程:
    • 内存平移16个字节得到cache_t的指针地址
    • 获取buckets以及mask
    • 哈希函数算出初始下标,得到初始bucket的地址
    • 从初始位置往前到头,然后从尾部到初始位置循环遍历查找sel
    • 找到的话走缓存命中,直接调用方法
    • 没找到走_lookUpImpOrForward,对返回的imp进行调用
  • 结合汇编源码进行真机汇编断点调试