通过这篇文章可以获得什么:
- 为什么要探索objc_msgSend?
- cache缓存读取,存储过程是怎样的?
- OC的编译时与运行时的概要分析
- Runtime运行时版本介绍
- 方法的调用方式都有什么?
- 方法的调用链路关系是怎样的
- 如何在代码层玩objc_msgSeng
- objc_msgSend汇编分析
- objc_msgSend汇编根据sel查找imp的流程
- 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)进行探索,本质上是发送消息,至于给谁发消息,怎么发消息,不由得就有了几个问题,也是接下来要探索的方向,暂时先列出来:
objc_msgSend它是被谁调用的,为了做什么事情?- 为什么
OC代码在编译的时候会被翻译成objc_msgSend,有什么优势? - 是否传递
参数,如果有,参数是怎么存储的? - objc_msgSend源码是怎么实现的?为什么选择
汇编,而不是C/C++? objc_msgSendSuper又是什么?
方法的调用方式
日常写代码在Objective-C Code层,很多的Framework、services都在这一层,Runtime System Library为底层相关的库,通过complier(编译器层)在中间层进行拦截,向上层Framework以及Runtime提供支持

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

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

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

方法的调用链路关系:

案例代码:
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
准备工作:
xcode设置:target->Build Settings->App Clang Perprocessing->Enable Strict Checking of objc_msgSend calls设置为NO,意思是关闭编译器对objc_msgSend的检查,让你在代码里面可以自由的使用objc_msgSend- 在文件中导入
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_msgSendCall、objc_msgSendCall、objc_msgSendSuperCall,第一个为纯Objective-C方法,第二个为objc_msgSend方法,向FFPerson发送消息,最后一个是objc_msgSendSuper,通过创建了一个FFBoys类继承了FFPerson,声明了一个likeGirls方法,并没有事项,然后通过objc_msgSendSuper向父类中查找方法实现,最终也是可以输出 -[FFPerson likeGirls]的。
结论:
- 编译
上层代码在中间层(C++)会对应一个解释,当法调用时,在中间层对应的是objc_msgSeng消息发送- 上层
Objective-C方法的调用在中间层会被翻译成objc_msgSend或者objc_msgSendSuper。- objc_msgSend方法构成:objc_msgSend(
消息的接收者,消息主体(sel+参数))super_class设置的是receiver,设置为谁,调用方法时候第一响应者就是谁。- 通过这个探索过程,
方法调用的本质是消息发送
objc_msgSend汇编分析
objc_msgSend汇编根据sel查找imp的流程
- 第一步:
cmp p0, #0p0为消息接收者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=p13、needs_auth=1、auth_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:
mov x15, x16,隐藏isa,将x16寄存器赋值到x15。
- 现在探索的是
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。
- 进入到
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
eor p12, p1, p1, LSR #7,因为p0寄存器是receiver,p1寄存器为第二个参数,SEL _cmd,所以p1=_cmd,对应上面的指令就可以得出p12 = (_cmd >> 7) ^ _cmd
and p12, p12, p11, LSR #48,p11 = chahe_t = _bucketsAndMaybeMask,可以翻译成p12 = p12 & (_bucketsAndMaybeMask >> 48),这个指令最终结果是找到已知buckets的index。
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: ldp p17, p9, [x13],将x13寄存器的值取出来放在p17和p9,因为x13为bucket_tj结构体,在arm64架构下,第一个值是imp,第二个值是sel,所以p17=imp,p9=sel。
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汇编的探索过程中,发现其实本质代码并不多,逻辑也很简单,相对于上层代码,汇编就很直接,难点在于不知道一些指令的含义,另一些伙伴望而却步。其实完全没有必要,晚一些时间,下一些功夫,就是没有基础的伙伴也是可以看懂的,看汇编最重要的一点
一定要平心静气,不能急,不能急,不能急。重要的事情说三遍。原所有在汇编遨游的伙伴都能如鱼得水。























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)