方法查找&杂谈

349 阅读15分钟

cache_t查找流程。

首先听课的时候,上来先懵逼,因为如我下文书所述,ARM64 寄存器只有x/w0~x/w30。

这个p 寄存器是什么👻?于是乎查资料,全都是风马牛不相干得解释。

于是开始怀疑自己,难道之前学的东西都是错了?

然后llvm是编译C/C++/OC的时候才能起作用,应该也不是查找的方向。

那么,只有一个解释,p寄存器隐含在源代码中 。于是乎,我随便复制了一个p0,在整个工程中 就这么一搜,soka,其实归根到底就是一个宏定义。

我一直不太明白__LP64__的含义,原来__LP64__是true ARM64的意思,说的是long 和 pointer类型都是64bit,而else的含义是只有long long 和 pointer是64bit ,但是long还是32bit ,这种情况下 ,全部取w寄存器(x寄存器低32bit)

整个查找的入口是 

ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

这些ENTRY和UNWIND 这些都是汇编中的宏,伪指令

like this

.macro UNWIND
	.section __LD,__compact_unwind,regular,debug
	PTR $0
	.set  LUnwind$0, LExit$0 - $0
	.long LUnwind$0
	.long $1
	PTR 0	 /* no personality */
	PTR 0  /* no LSDA */
	.text
.endmacro

NoFrame 是#define NoFrame 0x02000000 // no frame, no SP adjustment

这个no SP adjustment还真是触及到我知识盲区了,没听过,可以去研究一下。

首先判空  p0是x0 ,存放的是self。

为空执行LReturnZero 这里就是全部置0,这里,d是ARM64下面的浮点数寄存器。你声明一个CGRect,里面的四个浮点数就会分别存放在d0~d3四个寄存器中。

然后将self放入x13 

接下来是获取isa

对于ARM64架构来说SUPPORT_INDEXED_ISA为0

所以 这里就看这句话

// 64-bit packed isa

and	p16, $0, #ISA_MASK  //isa & ISA_MASK

0估计是第0个参数,虽然我没找到,但很明显,这个0估计是第0个参数,虽然我没找到,但很明显 ,这个0存的就是 self这个地址对应的值,也就是那个isa_t,然后and是与指令,相当于isa & ISA_MASK

然后继续CacheLookup 

LLookupStart$1:

ldr	p11, [x16, #CACHE] 基址变址 将x16也就是isa  加上偏移量#16 获取mask|buckets(cache_t) 放到x11中
2 * __SIZEOF_POINTER__ == 16 因为中间隔着superClass superClass是[x16, #8]

继续走CACHE_MASK_STORAGE_HIGH_16分支

and	p10, p11, #0x0000ffffffffffff	// 取低48位buckets
and	p12, p1, p11, LSR #48		// x12 = _cmd & mask p11右移动48位 剩下的是mask  x1是SEL 所以现在p12 = SEL & mask

add	p12, p10, p12, LSL #(1+PTRSHIFT) 这个 根据就是Hash取值

ldp	p17, p9, [x12]		ldpload pair的意思 取两个64bit,存放imp sel impx17 selx9 

cmp	p9, p1			
b.ne	2f			//     scan moreCacheHit $0	
判断当前拿到的buctetsel是否等于要查找的sel
等于 执行CacheHit 此时$0 = TailCallCachedImp
不等于 继续查找
__objc_msgSend_uncached 判断p9是否有sel cbz指令 空跳转 非空不跳转

cmp	p12, p10		// wrap if bucket == buckets
b.eq	3f
判断如果bucket == 数组首地址 
add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) //获取下标 然后偏移 怎么偏移 帅C讲的很清楚了

ldp	p17, p9, [x12]		// {imp, sel} = *bucketimp存放在x17 sel存放在x9

将指针移到末尾 然后从尾部开始 往前查找
执行循环 

直到CacheHit $0
未命中 跳出循环 - >JumpMiss->__objc_msgSend_uncached

时间比较仓促,待后续完善 

------------------------------------------华丽的分割线------------------------------------

 另外 ,之前断断续续听了一些片段,周末终于有空追一下录播了,我想同时也写一些追录播过程中的思考与杂谈。

主要涉及以下几个方面

1.联合体位域

2.汇编

3. 一些计算机的基础知识 

4.穿插着一些这几期课程内容的思考。

我们先来说联合体,概念网上一大堆。这里不当搬运工。

我们来看具体的例子。

我声明了这么一个成员变量,首先这个联合体有3个成员, bits 、test、和一个结构体。

一个联合体的大小是这个联合体中最大成员所占用的字节数,上面的例子,最大成员是这个结构体位域(2字节 ),所以,整个联合体的大小为两字节。

为什么结构体位域的大小是两字节,因为位域每一个成员占用3字节,类型为uchar,所以第一个字节可以存两个位域成员(front、back),剩下两个bit填充0(剩下两 bit存不下一个3bit的成员),而剩下的两个位域成员要从一个新的字节开始存储。所以 整个结构体需要占用两个字节。上述联合体占用内存示意图如下所示。 

打印结果如下,同时,我们捎带手儿查看一下联合体所占用的内存,验证一下结论。(后面的注释是我随手调换了代码顺序,但是注释没有同步更改,我当然知道0b101 = 5😆,大家不要纠结。)

 如上图所示,整个联合体内存中所存储的二进制位为 0x371D(0011 0111 0001 1101 )

实际上联合体只要对任何一个成员所占用的内存空间赋值,那么其他成员相对应的空间都会被赋值,因为联合体是公用内存空间。即使调用相应的构造函数,只要其他成员对应的内存位置被赋上了值,则获取其他相应成员的值 ,就会得到相应的结果。

这里还有一个字节序的知识点,就是我们常提到的大端对齐,小端对齐。 IOS是小端对齐,大家阅读源代码的时候,需要注意。

不当搬运工,只提供传送门。 

blog.csdn.net/yangcs2009/…

---------------------------------------- 华丽的分割线--------------------------------------

因为最新的一次课程涉及到了汇编,接下来,我想从汇编的角度,来探究一下test方法对应的实现方式,同时,会穿插着介绍一些汇编的相关知识。

这里我想首先纠正一个帅C老师在讲课过程中的小错误,我姑且认为是口误吧😆 ,我们说MacOS对应的硬件架构是什么?,并非是i386,而是X86_64,凡是你的Mac电脑采用的是Intel  64位处理器,那么他的硬件架构一定是X86_64,i386对应的是Intel 32位处理器的架构。 

首先,Assembly Language和硬件架构(Architecture)密切相关,这也是为什么Assembly Language不具备任何可移植性的原因, 所谓C/C++作为高级语言 ,可移植性强(Software Portability ),就是通过编译器,将高级语言转换成相应硬件架构对应的汇编语言,使同样的Code可以运行在不同的硬件平台。

比如,X86_64 的寄存器有rbp,rsp,rax等,而ARM64的寄存器则是x0~x30,哦对,这里帅C老湿还有一个口误,那就是ARM64没有x31或者x32寄存器。就到x30。对于一个汇编语言开发者来说,即使一个平台的汇编再熟练,他也不敢打保票说可以熟练掌握另一个平台的汇编。因为他要熟悉新架构的指令集,寄存器,和函数调用约定。

我们知道,现代计算机架构,运算只发生在CPU(也就是寄存器)中,内存只负责存储数据,完全不参与运算,汇编语言就是将内存中的数据,读取到寄存器,计算后,写回相应的寄存器,再写回内存中。大概的过程是,CPU通过指令总线发出存取内存的指令, 内存根据地址总线的地址,将相对应内存中的数据,通过数据总线传递给CPU。所以汇编语言,就是在不停的读写内存,计算地址。

最后,我不推荐大家上来就学习ARM64的汇编,因为学完了根本体会不到汇编语言的精髓,因为ARM64令人发指的阉割了push pop这两个经典的指令。学会汇编,可以提高你技术的天花板 ,我推荐给大家一本书。虽然这个地球上已经没有多少人用8086指令集写程序了,但是,这本书算是内功心法,你掌握了心法、精髓,那么所有的外家招式就都融会贯通了。

item.jd.com/12841436.ht…

ARM64有31个通用寄存器x0x31,均为64bit。(用到哪说到哪,其他的还有,但这里不会提及),还有以此衍生出来的w0w30寄存器,均为32bit,为x0~x30的低32位 。

举个例子 比方说x0里面存的是0x00000000d177f420,那么w0里面的值就是0xd177f420

首先,对于函数来讲 (OC底层都是函数,🌎人都知道 ),函数的调用要符合函数的调用约定(很重要),他决定了函数参数存放在哪些寄存器中,超出参数的 入栈的顺序,存放返回值的寄存器,以及是调用方实现堆栈平衡,还是被调用方实现堆栈平衡,

在ARM64中。

1.函数的形参存放到x0~x7。 超出的形参由从右往左的顺序依次入栈。

2.返回值存放在x0处。

3.超出的形参从右往左依次入栈。(本例涉及不到) 

4.被调用者实现堆栈平衡。

具体的大家可以度娘搜ATPCS调用约定,然后自己写个例子,就懂了。

我们知道函数的形参和局部变量是存放在栈空间内,所以,每发生一次函数调用,栈空间就要相应的开辟和回收,栈空间的开辟与回收不同于堆区,不是什么malloc、alloc、calloc,是通过栈帧的东西来控制,就是上图汇编中的sp(stack pointer),同时,ios系统中的栈空间是向低地址增长。对sp做减法,相当于开辟,做加法,相当于回收,所以<+0 >和<+52>行相当于实现堆栈平衡,同时,test作为被调用方(被 viewDidLoad调用),依照约定,由test函数实现堆栈平衡。

有了上述感性的认识,废话也说够了。直接上代码。

参数入栈之后内存分布大致如下

解释一下上述代码

看到那个#0x350,以及LLDB调试的打印结果,用帅C老师的话来说,是不是一摸摸,一样样的。

我们多写几句赋值

代码长了很多  but don't be suprised  很多代码都是重复的 我们有三个赋值语句 汇编就要找3次_direction的地址 执行 三次赋值操作

解释三处关键点

一句简单的_direction.front = 0b101 汇编要经过

ldr指令->self存寄存器、

adrp指令->计算基址 

add指令->计算成员变量偏移值存放地址

ldr指令 ->根据存放地址取偏移值 

add指令->self+成员变量偏移值取得真正的成员变量地址

and orr各种位运算

str指令->将cpu计算的结果存放回成员变量地址指向的内存空间

经过汇编分析,我们也可以验证之前所学的,类的实例在内存中的布局分布,首先是isa,之后是各种成员变量,由于我将代码写到了viewController里面,由于长继承链关系,导致_direction这个成员变量的偏移值距离self有点远。如果大家自己写一个类,直接继承于NSObject,会发现_direction 距离self只有8,汇编后也不需要adrp指令,简化了一些操作,大家可以实操验证一下。

不知道大家看到这里有没有意识到,你在高级语言层面写的一些花里胡哨儿的东西,在汇编这一层,已经没有多少C/C++等高级语言的影子,汇编没有类型(int float NSObject UIView),没有什么函数、方法,成员变量。对于汇编语言来说,最重要的是指令集和寄存器和相应的寻址方式 ,全部操作的是二进制数据, 基本就是读内存->偏移->运算->偏移-> 写内存。这里,只能说C/C++操作地址的偏移还残存着一些汇编的思想。 

总结一下上述涉及到的一些我认为有用的知识点 

针对上图补充一下

将位运算的结果 配合左右移,赋值给另外一个变量,相当于取了这个值对应的位,比如,将上述第一个例子 ,和0x0000FFFF"与"完之后,右移16位,是不是相当于取出了这个寄存器的高16位

ok,说到了位运算,我接下来想写一些关于isa与掩码这块的思考。

课上我们知道,大部分情况下,isa不是一个纯指针,他是isa_t ,一个联合体。

课堂上有同学问,为什么x86_64是44字节存放真正的isa地址,为什么ARM64下又是33字节存放真正的isa 地址呢?ok anyway,我们站在上帝视角,看苹果的Source Code,确实声明是这样声明的,但是我想接着帅C老师的结论,思考一下,为什么苹果这样设计,为什么一个 64bit的指针居然44bit(33bit)存放就够了。

首先引出一个知识点,叫虚拟内存空间。

我们知道,操作系统内核中的一项重要功能就是内存管理,我们知道macOS和IOS基于XNU内核打造,同时XNU内部有一个重要的核心模块,叫Mach,我相信帅C讲到想Runloop的时候一定会提到的,Mach正是macOS和IOS负责内存管理的模块。

我们现在所说的内存空间,实际上都是虚拟的地址,Mach核心负责管理一个映射表,这个映射表就是虚拟内存和实际物理内存的对应关系。我们所有的指针,变量,都要存放在这个虚拟的内存空间中。

不知道大家是否注意到了一个细节

只有这句宏定义后面有注释,那这个注释干吗用的,很显然,是给你看的对么,是苹果的工程师告诉你为什么44bit(33bit)就够用了,上文书提到,虚拟内存空间,是操作系统管理的虚拟地址和实际物理地址之间的映射,但是这个虚拟内存空间不是无限的,是有范围的,而且在不同硬件架构下面是不一样的 。

在X86_64下,范围是0~0x0x7fffffe00000,这个地址的含义就是Mach核心虚拟地址空间的范围,我们所有的指针地址的范围,都会落在这个内存空间范围内,我们来分析一下这个地址的玄机,这是一个12位16进制数,四位2进制组成一个16进制数,理论上需要48bit二进制位存储,好了,64一下变成48了,但是离44还差4位。别着急,我们往下分析,这个内存地址的最高位是7,也就是说,这个内存第12位16进制数无论多么大,最大是7,7的二进制是 0b0111, 所以,无论如何,最高位都是0,那你存他干嘛?所以48位变成了47位,我们结合之前学习到的,内存采用16bit对齐(实际上8bit就足够了),所以任何一个指针的地址一定是8的倍数,结合下图,这个指针的最后三位一定是000,ok,一定是0,你存他干嘛?所以47bit,最终只需要44bit就够了。

 shiftcls是什么,是指向元类的指针,这个不是位域,不是联合体了,这个实实在在是一个指针,所以这也是为什么isa敢在低三位存放

uintptr_t nonpointer : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1;这些成员的原因,这也是KC老师解释的为什么要shiftcls要>>3 一方面是因为前三位有nonpointer、 has_assoc、has_cxx_dtor。一方面是因为字节对齐 ,shiftcls是指针,8的倍数,最后三位一定是0,存他干嘛?还有,大家想象一下ISA_MASK 0x00007ffffffffff8ULL,最后是8 0x1000,一个值"与"0x1000,不就相当于末三位清0么。

 还有一个佐证,大家想想帅C老师上课时候的操作,先>>3 清零,再<<20位清零(相当于先 <<3还原,再<<17清高位的 0) ,但是接下来,又>>20,结果小翻车了一下。

后来帅C整理了思路,将刚才的>>20改成了>>17,成功的双击666,为什么是17,因为虽然你少存了3位,但是因为他是0,你取出来的时候,要补三个0的。

fine,写到这儿,ARM64是不是就大同小异了,他的虚拟地址空间的范围是0~0x1000000000,他也要字节对齐,他是一个10位的16进制数,但是大家注意,他最大到0x1000000000 ,也就是指针最大的范围一定小于0x1000000000(理论最大值0x1000000000 - 8) ,所以指针理论上只需要9位16进制数,36bit二进制位就够了,然后他也要字节对齐,也是8的倍数,再少存3位36 - 3 = 33,所以ARM64下33bit就够了。

大家可以抽空去打印一下在macOS和真机下面的指针,比如,你就写一个NSObject alloc init,你看看这两个平台的指针长得各有特别,肯定能看出些端倪。

另外,大家回忆一下buckets和mask,为毛一到ARM64下,就搞位域这个东西,是因为buckets也是指针,理论最大33位,一个64bit光存这一个东西,实在太浪费了,所以很多时候,都复用了地址空间。不得不说,苹果工程师的coding的境界,确实高了一个档次。 

水平有限 文中难免有错漏的地方 还请各位不吝赐教。