开始今天的内容之前先上个小工具
思考下面枚举变量的内存布局
enum TestEnum {
case test1, test2, test3
}
var t = TestEnum.test1
t = .test2
t = .test3
思考:上面三个赋值对内存有什么影响呢?
不妨打印一下看看:
MemoryLayout<TestEnum>.stride//分配的
MemoryLayout<TestEnum>.size//实际用的
MemoryLayout<TestEnum>.alignment
打印结果:都是1,也就是枚举变量占用1个字节
结论:如果枚举类型非常简单,仅仅是列举了N个case,那么这个枚举占用了1个字节. 0x00~0xFF
也就是可以定义256个case(一般不这么干哈)
那内存里面是如何存储的呢?
先看一下枚举变量t第一次赋值时,内存存储的是什么值?
image.png
再看一下枚举变量t第二次赋值时,内存存储的是什么值?
image.png
最后看一下枚举变量t第三次赋值时,内存存储是什么值?
image.png
t的三次被赋值分别存储的是:0、1、2
栗子1
有原始值的枚举变量占用多少内存呢?
enum TestEnum: Int{
case test1 = 1, test2 = 2, test3 = 3
}
var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))
t = .test2
t = .test3
print(MemoryLayout<TestEnum>.size)
print(MemoryLayout<TestEnum>.stride)
print(MemoryLayout<TestEnum>.alignment)
打印结果:都是1,也就是枚举变量占用1个字节.
同上,看一下初次赋值的枚举变量t的内存存储.
image.png
t的第二次赋值:
image.png
t的第三次赋值:
image.png
由此得出结论:原始值是不影响枚举变量的内存存储的.
栗子2
思考:有关联值的枚举占用内存又是如何的呢?
enum TestEnum{
case test1(Int, Int, Int )
case test2(Int, Int )
case test3(Int)
case test4(Bool)
case test5
}
var t = TestEnum.test1(1,2,3)
t = .test2(2,3)
t = .test3(6)
t = .test4(true)
t = .test5
先打印一下看结果:
print(MemoryLayout<TestEnum>.size)
print(MemoryLayout<TestEnum>.stride)
print(MemoryLayout<TestEnum>.alignment)
25
32
8
Program ended with exit code: 0
看到这个打印结果:有什么想法?
1、看看32个字节是什么?
2、看看25个字节是什么?
image.png
小端模式:高高低低
var t = TestEnum.test1(1,2,3)
01 00 00 00 00 00 00 00 0x0000000000000001
02 00 00 00 00 00 00 00 0x0000000000000002
03 00 00 00 00 00 00 00 0x0000000000000003
00
t = .test2(2,3)
01 00 00 00 00 00 00 00 0x0000000000000002
02 00 00 00 00 00 00 00 0x0000000000000003
03 00 00 00 00 00 00 00 0x0000000000000000
01
t = .test3(6)
01 00 00 00 00 00 00 00 0x0000000000000006
00 00 00 00 00 00 00 00 0x0000000000000000
00 00 00 00 00 00 00 00 0x0000000000000000
02
t = .test4(true)
01 00 00 00 00 00 00 00 0x0000000000000001
00 00 00 00 00 00 00 00 0x0000000000000000
00 00 00 00 00 00 00 00 0x0000000000000000
03
t = .test5
00 00 00 00 00 00 00 00 0x0000000000000000
00 00 00 00 00 00 00 00 0x0000000000000000
00 00 00 00 00 00 00 00 0x0000000000000000
04
由此得出结论:
如果一个枚举类型有关联值的话,会用1个字节来存储成员值,N个字节存储关联值(N取占用内存最大的关联值),任何一个case的关联值都共用这N个字节(类似于共用体的概念)
栗子1
enum TestEnum {
case test
}
MemoryLayout<TestEnum>.size//0
MemoryLayout<TestEnum>.stride//1
MemoryLayout<TestEnum>.alignment//1
从打印看:虽然编译器分配了1个字节的内存地址,但是并没有使用到,因为枚举里面就一个case,不需要内存区分哪一个case.
栗子2
enum TestEnum {
case test(Int)
}
MemoryLayout<TestEnum>.size//8
MemoryLayout<TestEnum>.stride//8
MemoryLayout<TestEnum>.alignment//8
从打印看:只需要8个字节存储关联值.
有木有这样的疑问: 为啥没有上面说的额外的1个字节呢?
因为那1个字节是用来区分不同的case,然鹅,这里只有一个case呀.
栗子3
enum TestEnum {
case test(Int)
case test1
}
MemoryLayout<TestEnum>.size//9
MemoryLayout<TestEnum>.stride//16
MemoryLayout<TestEnum>.alignment//8
1个字节存储成员值
8个字节存储关联值
switch语句底层实现
栗子1
enum TestEnum {
case test1, test2, test3
}
var e = TestEnum.test1
switch e {
case .test1:
print("test1")
case .test2:
print("test2")
case .test3:
print("test3")
}
先留一个问题:switch是如何实现跳转的?
栗子2
enum TestEnum {
case test1(Int, Int, Int)
case test2(Int, Int)
case test3(Int)
case test4(Bool)
case test5
}
var e = TestEnum.test1(10,20,30)
switch e {
case let .test1(v1, v2, v3):
print("test1", v1, v2 ,v3)
case let .test2(v1, v2):
print("test2", v1, v2 )
case let .test3(v1):
print("test3", v1)
case let .test4(v1):
print("test4", v1)
case let .test5:
print("test5")
}
试着猜测一下上面问题的答案:
至此我们都知道上面的枚举变量编译器给它分配了32个字节,实际使用25个字节.要想知道e符合哪一个case,肯定是看成员值,也就是第25个字节存储的值,看它存储的是多少(0 、1、2、3、4符合哪一个case).假如第25个字节存储的值是0,那么就可以把前24个字节赋值给let .test1(v1, v2, v3).
总结:只需要根据第25个字节就可以判断出是哪个case,然鹅字节跳到对应的case,最后绑定关联值就OK.
这都是猜测哈!!!不要骂我
var e = TestEnum.test1(10,20,30)
这句是函数调用吗?
不是,这是一种写法,枚举的语法糖.本质是内存的赋值操作.分配内存,同时给内存设置值.
程序的本质
软件的执行过程
软件执行过程
寄存器与内存
通常,CPU会先将内存中数据存储到寄存器中,然后再对寄存器中的数据进行运算.
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色的内存空间
- CPU首先红色内存空间的值放到rax寄存器中:
movq 红色内存空间, %rax
- 然后让rax寄存器与1相加:
addq $0x1, %rax
// mac上会加% - 最后将值赋值给内存空间:
movq %rax, 蓝色内存空间
image.png
image.png
image.png
image.png
以上也是这两行code
的诠释
var a = 3
var b = a + 1
编程语言的发展
机器语言:由0和1组成
汇编语言(Assembly Language):用符号代替了0和1,比机器语言便于阅读和记忆
高级语言:C\C++\Java\JavaScript\Python等,更接近人类自然语言
操作:将寄存器BX的内容送入寄存器AX
机器语言:1000100111011000
汇编语言:movw %bx, %ax
高级语言:ax=bx;
image.png
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令.
- 汇编语言可以通过编译得到机器语言,机器可以通过反汇编得到汇编语言.
- 高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言.
汇编语言的种类
随着CPU的发展,CPU的位数增多了,它对应的汇编是不一样的.汇编严重依赖于硬件设备,CPU的架构不同用的汇编就不同.
- 8086汇编(16bit)
- x86汇编(32bit)
- x64汇编(64bit)
- ARM汇编(嵌入式、移动设备)
- ...
x86、x64汇编根据编译器的不同,有2种书写格式
- Intel:Windows派系
- AT&T:Unix派系
作为iOS开发工程师,最主要的汇编语言是:
- AT&T汇编->iOS模拟器
- ARM汇编->iOS真机设备
常见的汇编指令.png
注意⚠️
movq -0x18(%rbp), %rax #将[%rbp-0x18]这个内存地址存储的数据取出来,赋值给rax.
leaq -0x18(%rbp), %rax #将[%rbp-0x18]这个内存地址值赋值给rax.
jmp 0x4001002
表示: 跳转到内存地址为0x4001002的内存代码里去执行
指令的内存地址是挨在一起的.
callq : 会和retq配合使用,跳到地址去执行代码,一般是函数地址
jmp : 跳转,跳到指定的地址一直往下走.
jmp *%rax call *%rax
间接跳转,也就意味着rax里面存储的是一个函数地址.
操作数长度? q:64位,8个字节.
AT&T: movq
Intel: mov
分析:movq $0xa, 0x1ff7(%rip)
将0xa这个值,放到rip+0x1ff7这个地址对应的存储空间.
思考:将0xa放到某个内存去,要知道花多少个内存空间(字节)来存储这个a.
mov 0xa, (0x110) ;在AT&T汇编中需要知道用多少字节存储
0x110 0xa
0x111 0x0 ;两个字节存储a
0x112
0x113
寄存器
32位寄存器,能存储4个字节
高位向低位做兼容
汇编指令里面会有r、e开头的寄存器
r开头的是8个字节64位,
e开头的是4个字节,32位
为了兼容2字节的,e拿出低16位
为了兼容1字节的
r开头:64bit 8字节
e开头:32bit 4字节
ax bx cx :16bit 2字节
bh bl:8bit 1字节
注:对象存在内存中,结构体最多8个字节.
LLDB常用指令
读取寄存器的值
register read/格式
修改寄存器的值
register write rax 0
读取内存中的值
x/数量-格式-字节大小 内存地址
比如: x/3xw 0x0000e0789
x/4xg 0x00000010
修改内存中的值
memory write 内存地址 数值
比如:memory write 0x00000010 10
格式
x是16进制,f是浮点,d是十进制
字节大小
b-byte 1字节
h-half word 2字节
w-word 4字节
g-giant word 8字节
expression 表达式
可以简写:expr 表达式
expression rax = 1
po 表达式
print 表达式
po/x $rax
以下三个指令是等价的
thread step-over 、next 、n
单步执行,把子函数当作整体一步执行(源码级别)
thread step-in、step 、s
单步执行,遇到子函数会进入子函数(源码级别)
thread step-inst-over 、nexti 、ni
单步执行,把子函数当作整体一步执行(汇编级别)
thread step-inst 、stepi 、si
单步执行,遇到子函数会进入子函数(汇编级别)
thread step-out、finish
直接执行完当前函数的所有代码,返回上一个函数(遇到断点会卡住)
rip存储的是指令地址
CPU要执行的下一条指令地址就存储在rip中
网友评论