进程内存布局
每个进程分配的内存由很多部分组成,通常称为“段”。
1.文本段:包含了进程运行的程序二进制机器语言指令,只读,可共享,因为多个进程可同时运行同一程序;
2.初始化数据段:包含显式初始化的全局变量和静态变量;
3.未初始化数据段:也称为BSS段,包含未进行显示初始化的全局变量和静态变量。为什么分开放呢?主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间,相反可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配这一空间。
4.栈段:动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,存储函数的局部变量、实参和返回值。动态存储函数之间的关系,以保证被调用函数在返回时恢复到母函数继续执行
5.堆:可在运行时为变量动态进行内存分配的一块区域,并在用完之后归还给堆区。
32位计算机限制了虚拟地址空间为4GB的大小。
栈与系统栈
栈在内存中的存放是高地址是栈底,低地址是栈顶。
内存的栈区实际上指的就是系统栈,系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似C语言这样的高级语言,系统栈的PUSH、POP等栈平衡细节是透明的。一般只有在使用汇编语言开发程序的时候,才需要和它直接打交道。
寄存器与函数栈帧
每一个函数都独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。栈驻留在内存的高端并向下增长(朝堆的方向)。
(1)ESP
栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
(2)EBP
基址指针寄存器,ESP就是一直指向栈顶的指针,而EBP只是存放某时刻的栈顶指针,以方便对栈的操作,如取局部变量、函数参数等。
函数栈帧
ESP和EBP之间的内存空间为当前栈帧,栈帧中一般包含以下几类信息:
(1)函数实参和局部变量:为函数局部变量开辟的内存空间。
(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过栈帧平衡计算得到),用于在本栈被弹出后恢复出上一个栈帧。
(3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。
ps:函数栈帧的大小并不固定,一般与其对应的局部变量多少有关。函数运行中,其栈帧大小也是不断变化的。
还有一个至关重要的寄存器是EIP,指令寄存器,其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。
函数调用约定与相关指令
函数调用约定描述了函数传递参数方式和栈帧工作方式的技术细节。不同操作系统、不同语言、不同编译器在实现函数调用原理虽然基本相同,但具体的调用约定还是有差别的,包括参数传递方式、参数入栈顺序是从右向左还是从左向右、函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行。
函数调用的步骤:
1.参数入栈:将参数从右向左依次压入系统栈中。
2.返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
3.代码区调转:处理器从当前代码区调转到被调用函数的入口处。
4.栈帧调整:
<1>保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
<2>将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
<3>给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
<4>对于_stdcall调用约定,函数调用时用到的指令序列大致如下:
push 参数3 ;假设该函数有3个参数,将从右向做依次入栈
push 参数2
push 参数1
call 函数地址 ;call指令将同时完成两项工作:a)向栈中压入当前指令地址的下一个指令地址,即保存返回地址。 b)跳转到所调用函数的入口处。
push ebp ;保存旧栈帧的底部
mov ebp,esp ;设置新栈帧的底部 (栈帧切换)
sub esp,xxx ;设置新栈帧的顶部 (抬高栈顶,为新栈帧开辟空间)
函数返回的步骤如下:
<1>保存返回值,通常将函数的返回值保存在寄存器EAX中。
<2>弹出当前帧,恢复上一个栈帧。具体包括:
(1)在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
(2)将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
(3)将函数返回地址弹给EIP寄存器。
<3>跳转:按照函数返回地址跳回母函数中继续执行。
还是以C语言和Win32平台为例,函数返回时的相关的指令序列如下:
add esp,xxx ;降低栈顶,回收当前的栈帧
pop ebp ;将上一个栈帧底部位置恢复到ebp
ret ;a)弹出当前栈顶元素,即弹出栈帧中的返回地址,至此,栈帧恢复到上一个栈帧工作完成。b)让处理器跳转到弹出的返回地址,恢复调用前代码区
一个很常见的堆栈帧如下:
i386 下的函数是这样调用的:
- 把所有或者一部分参数压入栈中,如果有其他参数没有入栈,则使用某些特定寄存器传递
- 把当前指令的下一条指令的地址压入栈中
- 跳转到函数体执行
- push ebp ;把 ebp 压入栈中
- mov ebp,esp ;ebp = esp (这是 ebp 指向栈顶,而此时栈顶就是 old ebp)
- [可选]sub esp, XXX ;在栈上分配XXX字节的临时空间
- [可选]push XXX ;保存名为 XXX寄存器(可重复多个)
下面是返回
- [可选]pop XXX ;如有必要,恢复保存过的寄存器(可重复多个)
- mov esp, ebp ;回复 ESP 同时回收局部变量空间
- pop ebp ;从栈中恢复保存的 ebp 的值
- ret ;从栈中取得返回地址,并跳转到该位置
下面是一个多级调用形成的堆栈格局: