主要是探讨下swift中枚举的内存布局,这里涉及到一些汇编的知识点方便查看信息。 我们先看看swift里面的枚举是长什么样子的,如图所示,定义了一个fruit(水果)的枚举
一、 初探枚举内存
enum fruit {
case apple,banana,pear
}
var f = fruit.apple
f = .banana
f = .pear
这个代码如果有点计算机代码基础的同学肯定都能看得懂,那么接下来我的第一个问题就是: 上段代码中的枚举变量f占用多少内存呢?
那么我们直接新建命令行项目,方便进行调试,这里可以通过MemoryLayout获取数据类型占用内存的大小,这里推荐李明杰写的一个是窥探内存细节的小工具:github.com/CoderMJLee/…
如图所示,现在对应的int类型占用8个字节,4个int就占用32个字节,再加上一个枚举的case占用1个字节,33个字节,但是对齐参数是8,所以要满足存储实际大小是33个字节的内存大小就需要放40个字节,必须是8的倍数。
此时我在这一行打个断点:
按照上述的步骤,将对应的地址赋值进去查看下具体的二进制,敲enter回车:
此时如图所示的00就是对接地址占用的内存空间 00 此时实际是放 f= .banana 这句代码还没执行,
由此可见,这个枚举变量只是占用一个字节, 数值分别就是 0,1,2
二、多变有意思的枚举类型
1、原始值rawValue的枚举
enum fruit:Int{
case apple = 1,case banana = 2, case pear = 4
}
如图所示,带有冒号接数据类型的原始值,我们都是知道的Int类型在64位系统里面占用8个字节,给人的初步印象仿佛是 将1 给对象 apple,2 给 banana, 3 给 pear,其实不是,只是代表我可以通过apple这个参数找到相对应的原始值.根据刚才读者自己的思路,发现其实我定义个枚举变量f,它实际占用1个字节,也就是说这个原始值,不影响我随便定义个一个枚举变量f,那么问题来了,这3个变量apple,banana,pear分别存储着什么值呢?
2、 关联值枚举
enum fruit {
case apple(Int,Int,Int)
case banana(Int,Int)
case pear(Int)
case other
}
上面的这个家伙就是关联值类型的枚举,下面我们来测下它的内存地址分布,老办法,直接打印
如图所示,我将对应地址copy到内存空间,查看0x0000000100006648 这个地址就是第一行,看标注的红框 每8个字节一个框,第一行7个不够就换行 ,那我们就看看,最开头的八个字节是啥,后面的8个字节又是啥,最后一行是清一色的00
不要紧,我直接把内存copy出来,对照我的自己写的代码 内存代码:
01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
程序的代码: var f = fruit.apple(1, 2, 5) 又因为这关联的是int类型,占用8个字节。那么我们这个时候可以将刚才的内存代码折叠下:
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
现在的cpu都是小端模式:高高低低 ,这下是不是有点豁然开朗的感觉啦
这样就很容易揣测出来当初你传的是1,2,5,那么是不是疑惑后面的全部都是00的是什么,其实刚才打印的时候看出来了,总的32个字节,我们实际用到了25个字节,也就是说:
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00
00 00 00 00 00 00 00 后面这个7是为了内存补齐
接下来我们代码再次走下一步
如图所示:走到代码是 f= .banana(3,4) 内存copy出来: 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
8个字节对齐整理下:
03 00 00 00 00 00 00 00
04 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01
00 00 00 00 00 00 00
这里就很奇怪了,这里最后一个字节为什么是01呢?带着这个问题我继续执行了下面的代码:f = .pear(6)
06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00
同样8个字节对齐整理下:
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
02
00 00 00 00 00 00 00
发现了什么? 哈哈,我把code代码也贴出来
有没有发现,我其实执行的上面代码:.apple,.banana,.pear 他们的标识位是0,1,2
那么这个时候我们就可以大胆的猜测下代码执行到print的时候,也就是走完f = .other 以后的内存布局。关联值都是00,又一个标识位是3.
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03
00 00 00 00 00 00 00
1 它会用1个字节存储成员值(标识位)
2 N个字节存储关联值(N:代表的是占用内存最大的关联值)
3 任何一个case都是用共用刚才最大N个字节,有一种共用体的感觉
4 如果关联值枚举中case只有一个,内存中就没必要给一个字节存储成员值,不存在标识位
二、利用Mems分析内存
获取变量f的内存数据
print(Mems.memStr(ofVal: &f))
看下面的打印:
0x0000000000000001 0x0000000000000002 0x0000000000000005 0x0000000000000000
前面 三组 8字节存储的是int, 1,2,5,后面的0x0000000000000000 存储的是case
接下来我们看这个:
这里工具是以8个为一个单位打印出来,如果喜欢以一个一个字节打印出来可以使用
print(Mems.memStr(ofVal:&e, alignment:.one))
这样的话,就一个个打印出来了 那么我再测试下:
三、汇编知识
程序的本质
通常cpu会将内存中的数据存储到寄存器中,然后将寄存器中的数据进行计算 举个🌰子: 假设内存中有块 黑色的空间的值是4,现在想把它的值加1 赋值到黄色内存空间
这时候cpu会将黑色内存空间中的值放到rax寄存器中
1 movq 黑色内存空间,%rax
2 addq $0x1,%rax
3 movq %rax,黄色空间
以上代码,就是将计算好了以后4+1=5以后的值再次赋值给内存空间
所以说先通过将数据拉到寄存器中计算以后再次放到内存中
汇编语言的种类:
8086汇编
x86汇编
x64汇编
ARM汇编(嵌入,移动设备)
x86和x64根据编译器的不同,有两种书写方式:
intel: Windows
AT&T: UNIX
作为ios开发 主要的汇编是:
AT&T汇编: ios模拟器
ARM汇编: ios真机
常见的汇编指令
操作名称 AT&T 说明
寄存器命名 %rax
操作数顺序 movq %rax,%rdx 将rax的值赋值给rdx
立即数 movq %0x10,%rax 将0x10赋值给rax
内存赋值 movq $0xa,0x1ff7(%rip) 将0xa赋值给地址为rip+0x1ff7内存空间
取内存值 leaq -0x18(%rbp),%rax 将rbp-0x18这个地址值赋值给rax
jmp指令 jmp *rdx call和jmp的写法类似
寄存器的具体用途
1 %rax常作为函数的返回值
2 %rdi,%rsi %rdx %rcx %r8 %r9 等寄存器常用存放函数参数
3 %rsp, %rbp 常作 栈操作
例子来了:
在testFunc() 调用的地方打上断点
这个时候输入命令:si 进入到函数中 所以以后call后面跟的一般是函数地址,跳转到这个地址,后面就有retq 两个配合使用
一步步的si执行下去后,最后retq以后回到了callq以后的下一行继续走
这里看段代码
movq 0xa,(0x110)
这段代码什么意思? 将 0xa这个值放到 0x110这个地址空间中去,那么问题来了
0x110
0x111
0x112
0x113
你是拿1个字节存储0xa,2个字节,还是拿4个字节存储0xa呢? 因为是小端模式,如果是拿1个字节存储0xa,那就是0xa 如果拿2个字节存储0xa, 那就是0xa 0x0.如果拿4个字节存储0xa 0x0 0x0 0x0
所以movq 这个q 代表是操作数的长度
movb (8bit)
movw (16bit)
movq (64bit)
16个常见的寄存器
还把刚才的汇编代码贴上来:
有以e开头的是32位的寄存器,4个字节
我们现在大家用的当然是64位的,但是当年都是从16位,32位 进化过来的,那么导致现在的64位的系统怎么去兼容 低版本的呢 来,我们先看看这个图:
lldb常用命令
1 读取寄存器的值
register read/格式
2 写入寄存器
register write
这里就可以直接写入register write rax 11,就直接修改了rax的值了
这里有个比较难懂的地方: 读取内存中的值: x/数量-格式-字节大小-内存地址 x/3xw 0x0000010(3:显示三组数据 x:16进制 w:字节大小)
x:16进制 f:浮点 d 10进制
字节大小:b 1个字节 h 2个字节 w 4个字节 g 8个字节
x/4xg就是如图所示: