美文网首页
汇编分析枚举的内存布局

汇编分析枚举的内存布局

作者: AndyYaWei | 来源:发表于2020-08-10 08:46 被阅读0次

开始今天的内容之前先上个小工具
思考下面枚举变量的内存布局

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 expressionrax = 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中

相关文章

  • 谈谈Swift中的枚举内存布局

    在掘金上看到从 汇编 到 Swift 枚举内存 的惊鸿一瞥之后,作者分析了几种不同枚举的内存布局,但是我感觉覆盖的...

  • 汇编分析枚举的内存布局

    开始今天的内容之前先上个小工具思考下面枚举变量的内存布局 思考:上面三个赋值对内存有什么影响呢?不妨打印一下看看:...

  • 从零学习Swift 03:汇编分析枚举内存

    今天我们用汇编分析一下枚举的内存布局. 一:普通枚举 通过打印结果看到普通枚举类型的枚举变量只占用了一个字节,那么...

  • Swift 中的枚举

    本文主要从内存和汇编去分析枚举的关联值和原始值 枚举成员值 用法 枚举的声明如下: 使用: 内存分析 通过Memo...

  • Swift~汇编分析枚举的内存布局

    本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗...

  • 谈谈Swift中的枚举内存布局

    谈谈Swift中的枚举内存布局谈谈Swift中的枚举内存布局

  • iOS-Swift-枚举变量的内存布局

    枚举章节讲了下枚举,本文就详细分析枚举变量的内存布局。 创建命令行项目,执行如下代码: 点击View Memory...

  • Swift汇编分析闭包-调用原理

    在《Swift汇编分析闭包-内存布局》[https://www.jianshu.com/p/bc5c595950c...

  • swift记录之观察枚举的内存分布

    内存分析 思考枚举变量的内存布局 所以从图上可以看出 关联值的内存分布 总结: ,上述中24>16>8>1所以最大...

  • Swift汇编分析闭包-内存布局

    1、闭包表达式与闭包 闭包表达式也就是定义一个函数。一般我们可以通过func定义一个函数,也可以通过闭包表达式定义...

网友评论

      本文标题:汇编分析枚举的内存布局

      本文链接:https://www.haomeiwen.com/subject/bhkerktx.html