@qidiandasheng
2021-04-21T10:59:39.000000Z
字数 5078
阅读 2520
iOS理论
原文链接:iOS 堆和栈的区别?
编译器自动分配释放 ,存放函数的参数值,局部变量的值,非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始终指向栈帧的顶部。
EBP寄存器又被称为帧指针(Frame Pointer)
ESP寄存器又被称为栈指针(Stack Pointer)
我们这里用以一段C程序源程序为例:
int fun(int a,int b);
int m = 10;
int main()
{
int i = 4;
int j = 5;
m = fun(i,j);
return 0;
}
int fun(int a,int b)
{
int c = 0;
c = a + b;
return c;
}
eip永远指向代码区将要执行的下一条指令,它的管控方式有两种,一种是“顺序执行”,即程序执行完一条指令后自动指向下一条执行;另一种是跳转,也就是执行完一条跳转指令后跳转到指定的位置。
ebp和esp用来管控栈空间,ebp指向栈底,esp指向栈顶,在代码区中,函数调用、返回和执行伴随着不断压栈和清栈,栈中数据存储和释放的原则是后进先出。
eip指向main函数的第一条指令,此时程序还没有运行,栈空间里还没有数据,ebp和esp指向的位置是程序加载时内核设置的。
程序开始执行main函数第一条指令,eip自动指向下一条指令。第一条指令的执行,致使ebp的地址值被保存在栈中,保存的目的是本程序执行完毕后,ebp还能返回现在的位置,复原现在的栈。随着ebp地址值的压栈,esp自动向栈顶方向移动,它将永远指向栈顶。
程序继续执行,开始构建main函数自己的栈,ebp原来指向的地址值已经被保存了,它被腾出来了,用来看管main函数的栈底,此时它和esp是重叠的。
程序继续执行,eip指向下一条指令,此次执行的是局部变量i的初始化,初始值4被存储在栈中,esp自动向栈顶方向移动。j同样流程。
上面两个局部数据都是供main函数自己用的,接下来调用fun函数时压栈的数据虽然也保存在main函数的栈中,但它们都是供fun函数用的。可以说fun函数的数据,一半在fun函数中,一半在主调函数中。
先执行传参的指令,此时参数入栈的顺序和代码中传参的书写顺序正好相反,参数b先入栈,数值是main函数中局部变量j的数值5。
程序继续执行,参数a被压入栈中,数值是局部变量i的数值4
程序继续执行,此次压入的是fun函数返回值,将来fun函数返回之后,这里的值会传递给m
跳转到fun函数去执行,这一步分为两部分动作,一部分是把fun函数执行后的返回地址压入栈中,以便fun函数执行完毕后能返回到main函数中继续执行。
另一部分就是跳转到被调用的函数的第一条指令去执行
第一件事就是保存ebp指向的地址值,此时ebp指向的是main函数的栈底,保存的目的是在返回时恢复main函数栈底的位置,这和前面main函数刚开始执行时第一步就保存ebp的地址值的目的是一样的。
程序继续执行,仍然使用腾出来的ebp看管栈底,ebp和esp此时指向相同的位置
局部变量c开始初始化,入栈,数值为0,这个c就是fun函数的数据,存在于fun函数的栈中
程序继续执行,fun函数中局部变量c的数据当成返回值返回
现在fun函数已经执行完毕,要恢复main函数调用fun函数的现场,这一现场包括两个部分,一部分是main函数的栈要恢复,包括栈顶和栈底,另一部分是要找到fun函数执行后的返回地址,然后再跳转到那里继续执行。
我们来看ebp的恢复。前面存储了ebp的地址值,现在可以把存储的地址值赋值给ebp,使之指向main函数的栈底。
ebp地址值出栈后,esp自动退栈,指向fun函数执行后的返回地址,之后执行ret指令,即返回指令,把地址值传给eip,使之指向fun函数执行后的返回地址。
恢复现场以后,把fun函数返回值传递给m
参数和返回值清栈:
main函数清栈:
这里我们打开xcode,运行以上C程序查看线程的调用栈。
int fun(int a,int b);
int m = 10;
int main()
{
int i = 4;
int j = 5;
m = fun(i,j);
return 0;
}
int fun(int a,int b)
{
int c = 0;
c = a + b;
return c;
}
(lldb) dis
test`main:
0x10fb331b0 <+0>: pushq %rbp //rbp帧指针入栈
0x10fb331b1 <+1>: movq %rsp, %rbp //rbp帧指针指向当前rsp栈指针
0x10fb331b4 <+4>: subq $0x10, %rsp //栈分配空间(16个字节)
0x10fb331b8 <+8>: movl $0x0, -0x4(%rbp) //i变量入栈4字节
0x10fb331bf <+15>: movl $0x4, -0x8(%rbp) //j变量入栈4字节
0x10fb331c6 <+22>: movl $0x5, -0xc(%rbp) //设置fun函数返回值位置4字节
-> 0x10fb331cd <+29>: movl -0x8(%rbp), %edi //edi通用寄存器保存j的值
0x10fb331d0 <+32>: movl -0xc(%rbp), %esi //esi通用寄存器保存i的值
0x10fb331d3 <+35>: callq 0x10fb331f0 ; fun at main.m:18
0x10fb331d8 <+40>: xorl %ecx, %ecx
0x10fb331da <+42>: movl %eax, 0x7358(%rip) ; m
0x10fb331e0 <+48>: movl %ecx, %eax
0x10fb331e2 <+50>: addq $0x10, %rsp
0x10fb331e6 <+54>: popq %rbp
0x10fb331e7 <+55>: retq
0x10fb331e8 <+56>: nopl (%rax,%rax)
我们看到subq $0x10, %rsp
这句汇编代码就是分配栈空间,原来rsp=rbp= 0x00007ffee00cccc0
,现在往低地址分配内存rsp = rsp - 0x10 = 0x00007ffee00cccb0
(16个字节的空间,一个字节表示两个16进制位,所以0x10有16种表示方式,也就是说需要占用16字节空间)。
(lldb) register read rbp
rbp = 0x00007ffee00cccc0
(lldb) register read rsp
rsp = 0x00007ffee00cccb0
我们能看到,i、j以及fun函数的返回一共只占用12个字节,为什么分配了16个字节呢?因为按照字节对齐,64位系统在分配内存的时候就是按照16字节的倍数进行分配的。
(lldb) dis
test`fun:
0x10fb331f0 <+0>: pushq %rbp
0x10fb331f1 <+1>: movq %rsp, %rbp
0x10fb331f4 <+4>: movl %edi, -0x4(%rbp)
0x10fb331f7 <+7>: movl %esi, -0x8(%rbp)
-> 0x10fb331fa <+10>: movl $0x0, -0xc(%rbp)
0x10fb33201 <+17>: movl -0x4(%rbp), %eax
0x10fb33204 <+20>: addl -0x8(%rbp), %eax
0x10fb33207 <+23>: movl %eax, -0xc(%rbp)
0x10fb3320a <+26>: movl -0xc(%rbp), %eax
0x10fb3320d <+29>: popq %rbp
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函数返回后要执行的下一个指令的地址。
(lldb) register read rbp
rbp = 0x00007ffee00ccca0
(lldb) memory read 0x00007ffee00ccca0
0x7ffee00ccca0: c0 cc 0c e0 fe 7f 00 00 d8 31 b3 0f 01 00 00 00 .........1......
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函数返回后要执行的下一个指令的地址。