堆栈和栈帧
最近看CSAPP,感觉有疑惑,顺便总结一下。
1.堆栈
我们平时说的堆栈其实是指栈,而实际上堆和栈是两种不同的内存分配。简单罗列一下各方面的异同点。
1).堆需要用户在程序中显式申请,栈不用,由系统自动完成。申请/释放堆内存的API,在C中是malloc/free,在C++中是new/delete。申请与释放一定要配对使用,否则会造成内存泄漏(memory leak),久而久之系统就无内存可用了,出现OOM(Out Of Memory)错误。一般在return/exit或break/continue等语句时容易忘记释放内存,所以检查内存泄漏的代码时要关注这些语句,看它们前面是否有必要的释放语句free/delete。
2).堆的空间比较大,栈比较小。所以申请大的内存一般在堆中申请;栈上不要有较大的内存使用,比如大的静态数组;而且除非算法必要,否则一般不要使用较深的迭代函数调用,那样栈消耗内存会随着迭代次数的增加飞涨。
3).关于生命周期。栈较短,随着函数退出或返回,本函数的栈就完成了使用;堆就要看什么时候释放,生命周期就什么时候结束。
说了这么多,我们发现解析Coredump还是跟栈的关系相对紧密,跟堆的关系是有一种产生Coredump的原因是访问堆内存出错。
2.i386的栈帧
继续谈栈帧布局,这次说说x86的栈帧布局和操作方法,见栈帧布局图:
上图描述的是一般x86的stack frame栈帧布局方式,当前帧为当前函数(被调用者)的stack frame,调用者的帧为调用函数(调用者)的stack frame。栈底在高地址,栈向下增长。图中,ebp(base pointer)就是栈基址,它指向当前函数的栈帧起始地址;esp(stack pointer)则是当前函数的栈指针,它指向栈顶的位置。压栈的顺序依次为栈基址ebp、其它寄存器、本地变量和临时变量。注意所传递的参数和返回地址lr一般放在调用者的栈帧中,当然按照实际的压栈过程,也有人认为previous ebp也在调用者栈帧内,这个暂时影响不大。
相比于MIPS,x86比较可爱的地方是刚刚提到的两点,就是可以用栈基址和栈指针明确标示栈帧的位置,栈指针esp一直移动,同时压栈的顺序有一定的规律,一个栈空间内的地址前面,必然有一个代码地址(lr)明确标示着调用函数位置内的某个地址。
3.i386的栈操作
x86有一对表示栈底和栈顶的寄存器,寄存器ebp是栈基址指针,指栈帧的底部(高地址),寄存器esp是栈指针,指栈帧的顶部(地址地)。
函数的返回结果是通过寄存器eax传递的,因此在函数退出前会将计算结果拷贝到eax中,然后再出栈返回调用者。
再来关注几个常用的函数内外跳转指令:
call: 调用一个函数。以寄存器和偏移量来调用函数和c++里的虚函数调用很类似
ret: 从一个函数返回。_stdcall调用规范要求如果有返回值,就要将返回值从堆栈的栈顶弹出
leave:是move ESP EBP/pop EBP的简写,用来退出函数,同时释放了为局部变量分配的空间
jmp: 无条件跳转
je: 如果相等则跳转
jne: 如果不等则跳转
顺便提一下数据的传递操作mov和movl,mov是将右操作数复制到左操作数,而movl传递方向相反,是将左操作数复制到右操作数。装入有效地址指令(即用来得到局部变量和函数参数的指针)lea和leal也一样。
最后,x86的压栈出栈指令众人皆知,分别是push和pop,或者其各种变体,比如puchad/popad是对所有通用寄存器的栈操作。