[关闭]
@qidiandasheng 2021-04-21T02:59:39.000000Z 字数 5078 阅读 2291

堆和栈(😁)

iOS理论


原文链接:iOS 堆和栈的区别?

前言

7271477-6826f45e95473767.png-24.8kB

编译器自动分配释放 ,存放函数的参数值,局部变量的值,非OC对象(基础数据类型)等。其操作方式类似于数据结构中的栈,内存地址连续向下增长。

OC对象,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,内存地址向上增长。

内存管理

移动设备的内存及其有限,每一个APP所能占用的内存是有限制的

什么行为会增加APP的内存占用

内存管理范围

引入堆和栈的概念

所以问题就来了,为什么OC对象需要进行内存管理,而其它非对象类型比如基本数据类型就不需要进行内存管理呢?

只有OC对象才需要进行内存管理的本质原因:

Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,就是release
OC对象存放于堆里面(堆内存要程序员手动回收)

非OC对象一般放在栈里面(栈内存会被系统自动回收)

堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存

举例说明

该代码块在内存中的表现形式如下图:

图中可以看到,栈里面存放的是非对象的基本数据类型,堆内存存放着oc对象

当代码块一过,里面的a,b,*c指针都会被系统编译器自动回收,因为它存放在栈里面,而OC对象则不会被系统回收,因为它存放堆里面,堆里面的内存是动态存储的,所以需要程序员手动回收内存

区别

对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理

对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露

  • 堆是动态分配和回收内存的,没有静态分配的堆

  • 栈有两种分配方式:静态分配和动态分配
    静态分配: 系统编译器完成的,比如局部变量的分配
    动态分配: 是由alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动态分配也由系统编译器进行释放,不需要程序员手动管理

运行时栈帧

什么是栈帧

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。简言之,栈帧就是利用栈帧指针,请注意不是ESP)寄存器访问局部变量、参数、函数返回地址等的手段。

每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:

栈是从高地址向低地址延伸,一个函数的栈帧用EBP和ESP这两个寄存器来划定范围。EBP指向当前栈帧的底部,ESP始终指向栈帧的顶部。

我们这里用以一段C程序源程序为例:

  1. int fun(int a,int b);
  2. int m = 10;
  3. int main()
  4. {
  5. int i = 4;
  6. int j = 5;
  7. m = fun(i,j);
  8. return 0;
  9. }
  10. int fun(int a,int b)
  11. {
  12. int c = 0;
  13. c = a + b;
  14. return c;
  15. }

eip & ebp & esp

eip永远指向代码区将要执行的下一条指令,它的管控方式有两种,一种是“顺序执行”,即程序执行完一条指令后自动指向下一条执行;另一种是跳转,也就是执行完一条跳转指令后跳转到指定的位置。

ebp和esp用来管控栈空间,ebp指向栈底,esp指向栈顶,在代码区中,函数调用、返回和执行伴随着不断压栈和清栈,栈中数据存储和释放的原则是后进先出。

截屏2021-04-20 上午8.13.31.png-169.5kB

运行时初始状态

eip指向main函数的第一条指令,此时程序还没有运行,栈空间里还没有数据,ebp和esp指向的位置是程序加载时内核设置的。

截屏2021-04-20 上午8.16.51.png-223kB

保存ebp

程序开始执行main函数第一条指令,eip自动指向下一条指令。第一条指令的执行,致使ebp的地址值被保存在栈中,保存的目的是本程序执行完毕后,ebp还能返回现在的位置,复原现在的栈。随着ebp地址值的压栈,esp自动向栈顶方向移动,它将永远指向栈顶。

截屏2021-04-20 上午8.18.45.png-425.4kB

构建main函数栈

程序继续执行,开始构建main函数自己的栈,ebp原来指向的地址值已经被保存了,它被腾出来了,用来看管main函数的栈底,此时它和esp是重叠的。

截屏2021-04-20 上午8.21.23.png-397.4kB

变量i和j压栈并初始化

程序继续执行,eip指向下一条指令,此次执行的是局部变量i的初始化,初始值4被存储在栈中,esp自动向栈顶方向移动。j同样流程。

截屏2021-04-20 上午8.23.00.png-415.2kB

传参

上面两个局部数据都是供main函数自己用的,接下来调用fun函数时压栈的数据虽然也保存在main函数的栈中,但它们都是供fun函数用的。可以说fun函数的数据,一半在fun函数中,一半在主调函数中。

先执行传参的指令,此时参数入栈的顺序和代码中传参的书写顺序正好相反,参数b先入栈,数值是main函数中局部变量j的数值5。

截屏2021-04-20 上午8.25.15.png-407.2kB

程序继续执行,参数a被压入栈中,数值是局部变量i的数值4

截屏2021-04-20 上午8.25.57.png-431.7kB

设置fun函数返回值位置

程序继续执行,此次压入的是fun函数返回值,将来fun函数返回之后,这里的值会传递给m

截屏2021-04-20 上午8.29.53.png-453.2kB

fun函数执行后的返回地址压栈

跳转到fun函数去执行,这一步分为两部分动作,一部分是把fun函数执行后的返回地址压入栈中,以便fun函数执行完毕后能返回到main函数中继续执行。

截屏2021-04-20 上午8.31.17.png-460.5kB

另一部分就是跳转到被调用的函数的第一条指令去执行

截屏2021-04-20 上午8.32.18.png-454.8kB

fun函数开始执行

第一件事就是保存ebp指向的地址值,此时ebp指向的是main函数的栈底,保存的目的是在返回时恢复main函数栈底的位置,这和前面main函数刚开始执行时第一步就保存ebp的地址值的目的是一样的。

截屏2021-04-20 上午8.33.51.png-472.8kB

构建fun函数栈

程序继续执行,仍然使用腾出来的ebp看管栈底,ebp和esp此时指向相同的位置

截屏2021-04-20 上午8.34.52.png-457.1kB

局部变量c压栈

局部变量c开始初始化,入栈,数值为0,这个c就是fun函数的数据,存在于fun函数的栈中

截屏2021-04-20 上午8.35.47.png-486.1kB

加法运算

截屏2021-04-20 上午8.36.52.png-476.8kB

返回值返回

程序继续执行,fun函数中局部变量c的数据当成返回值返回

截屏2021-04-20 上午8.37.54.png-458kB

恢复main函数栈

现在fun函数已经执行完毕,要恢复main函数调用fun函数的现场,这一现场包括两个部分,一部分是main函数的栈要恢复,包括栈顶和栈底,另一部分是要找到fun函数执行后的返回地址,然后再跳转到那里继续执行。

我们来看ebp的恢复。前面存储了ebp的地址值,现在可以把存储的地址值赋值给ebp,使之指向main函数的栈底。

截屏2021-04-20 上午8.39.24.png-465.1kB

ebp地址值出栈后,esp自动退栈,指向fun函数执行后的返回地址,之后执行ret指令,即返回指令,把地址值传给eip,使之指向fun函数执行后的返回地址。

截屏2021-04-20 上午8.40.10.png-469.5kB

返回值赋值给m

恢复现场以后,把fun函数返回值传递给m

截屏2021-04-20 上午8.41.16.png-457.5kB

清栈

参数和返回值清栈:

截屏2021-04-20 上午8.42.07.png-392.6kB

main函数清栈:

截屏2021-04-20 上午8.42.16.png-377.9kB

iOS里的线程调用栈

源程序

这里我们打开xcode,运行以上C程序查看线程的调用栈。

  1. int fun(int a,int b);
  2. int m = 10;
  3. int main()
  4. {
  5. int i = 4;
  6. int j = 5;
  7. m = fun(i,j);
  8. return 0;
  9. }
  10. int fun(int a,int b)
  11. {
  12. int c = 0;
  13. c = a + b;
  14. return c;
  15. }

输出当前main函数的汇编代码

  1. (lldb) dis
  2. test`main:
  3. 0x10fb331b0 <+0>: pushq %rbp //rbp帧指针入栈
  4. 0x10fb331b1 <+1>: movq %rsp, %rbp //rbp帧指针指向当前rsp栈指针
  5. 0x10fb331b4 <+4>: subq $0x10, %rsp //栈分配空间(16个字节)
  6. 0x10fb331b8 <+8>: movl $0x0, -0x4(%rbp) //i变量入栈4字节
  7. 0x10fb331bf <+15>: movl $0x4, -0x8(%rbp) //j变量入栈4字节
  8. 0x10fb331c6 <+22>: movl $0x5, -0xc(%rbp) //设置fun函数返回值位置4字节
  9. -> 0x10fb331cd <+29>: movl -0x8(%rbp), %edi //edi通用寄存器保存j的值
  10. 0x10fb331d0 <+32>: movl -0xc(%rbp), %esi //esi通用寄存器保存i的值
  11. 0x10fb331d3 <+35>: callq 0x10fb331f0 ; fun at main.m:18
  12. 0x10fb331d8 <+40>: xorl %ecx, %ecx
  13. 0x10fb331da <+42>: movl %eax, 0x7358(%rip) ; m
  14. 0x10fb331e0 <+48>: movl %ecx, %eax
  15. 0x10fb331e2 <+50>: addq $0x10, %rsp
  16. 0x10fb331e6 <+54>: popq %rbp
  17. 0x10fb331e7 <+55>: retq
  18. 0x10fb331e8 <+56>: nopl (%rax,%rax)

我们看到subq $0x10, %rsp这句汇编代码就是分配栈空间,原来rsp=rbp= 0x00007ffee00cccc0,现在往低地址分配内存rsp = rsp - 0x10 = 0x00007ffee00cccb0(16个字节的空间,一个字节表示两个16进制位,所以0x10有16种表示方式,也就是说需要占用16字节空间)。

  1. (lldb) register read rbp
  2. rbp = 0x00007ffee00cccc0
  3. (lldb) register read rsp
  4. rsp = 0x00007ffee00cccb0

我们能看到,i、j以及fun函数的返回一共只占用12个字节,为什么分配了16个字节呢?因为按照字节对齐,64位系统在分配内存的时候就是按照16字节的倍数进行分配的。

输出fun函数的汇编代码

  1. (lldb) dis
  2. test`fun:
  3. 0x10fb331f0 <+0>: pushq %rbp
  4. 0x10fb331f1 <+1>: movq %rsp, %rbp
  5. 0x10fb331f4 <+4>: movl %edi, -0x4(%rbp)
  6. 0x10fb331f7 <+7>: movl %esi, -0x8(%rbp)
  7. -> 0x10fb331fa <+10>: movl $0x0, -0xc(%rbp)
  8. 0x10fb33201 <+17>: movl -0x4(%rbp), %eax
  9. 0x10fb33204 <+20>: addl -0x8(%rbp), %eax
  10. 0x10fb33207 <+23>: movl %eax, -0xc(%rbp)
  11. 0x10fb3320a <+26>: movl -0xc(%rbp), %eax
  12. 0x10fb3320d <+29>: popq %rbp
  13. 0x10fb3320e <+30>: retq

进入add函数之后读取当前rbp帧指针的地址(rsp==rbp),然后通过memory read读取指针存储的值。

我们发现c0 cc 0c e0 fe 7f 00 00其实就是上一个FP的指针地址,d8 31 b3 0f 01 00 00 00是我们fun函数返回后要执行的下一个指令的地址。

  1. (lldb) register read rbp
  2. rbp = 0x00007ffee00ccca0
  3. (lldb) memory read 0x00007ffee00ccca0
  4. 0x7ffee00ccca0: c0 cc 0c e0 fe 7f 00 00 d8 31 b3 0f 01 00 00 00 .........1......
  5. 0x7ffee00cccb0: d8 cc 0c e0 05 00 00 00 04 00 00 00 00 00 00 00 ................

我们看到现在rbp的地址跟main函数栈底相差32个字节,0x00007ffee00cccc0 - 0x00007ffee00ccca0 = 32字节,其中16字节就是main函数的栈帧大小。另外16字节其中8字节保存的是main函数栈底的地址,8字节保存的是fun函数返回后要执行的下一个指令的地址。

参考

一文读懂iOS线程调用栈原理

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注