内存空间
- 栈(Stack) :栈保存函数的局部变量(不包括 static 修饰的变量),参数以及返回值。栈是后进先出(LIFO)的数据结构。栈属于静态内存分配。
- 堆(Heap) :堆保存函数内部动态分配(malloc 或 new)的内存,是另外一种用来保存程序信息的数据结构。堆是先进先出(FIFO)数据结构。堆属于动态内存分配。
- BSS段(Block Started By Symbol Segment ) :用来存放程序中未初始化和初始化为 0的全局变量的一块内存区域,在程序载入时由内核清零。BSS段属于静态内存分配。
- 数据段(Data Segment) :通常用来存放程序中已初始化的(非 0)全局变量和静态局部变量。数据段属于静态内存分配。
- 代码段(Code Segment/Text Segment) :用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 代码段属于静态内存分配。
什么是栈帧?
栈帧是栈的组成部分,一个栈由多个栈帧依序“堆叠”组成。
栈帧是指函数在被调用时,所拥有的一块独立的用于保存函数状态和变量的栈空间。每个函数都对应一个栈帧。同一个函数多次调用,每次可能会分配到不同的栈帧。
假设一个引用占用4个字节,则栈的结构如下图所示:
使用的寄存器
ebp:基址指针寄存器,指向栈帧的底部(高地址)。esp:栈指针寄存器,指向栈帧的顶部(低地址)。eip:指令指针寄存器,指向即将执行的程序指令的地址。
函数调用
一个函数调用,一般需要以下步骤,以stdcall进行函数调用func(a,b,c)为例:
-
保存函数的实参:
父函数调用时,会把参数从右至左入栈,实现保存函数实参的功能。
push c push b push a -
保存子函数结束后,需要返回的地址(返回到哪里):
执行
call指令。call funccall指令实际上做了两个工作,一个是将这个call指令的下一条语句入栈,实现返回地址的保存。然后把执行流跳转到函数里。所以一个call指令从功能上可以拆分为以下两个指令。push 本call指令下一条指令的地址 jmp func -
保存父函数的栈帧信息:
执行流到了
func函数内部,会先进行父函数栈帧信息的保存。此时esp和ebp依然维持父函数的栈帧。 保存父函数的ebp。push ebp当前子函数所有的栈中变量被释放后,
esp会回到函数调用前的状态,因此无需保存esp,只要保存ebp的信息即可。 -
在栈上开辟空间供局部变量使用:
子函数的栈帧底部变到esp处。
mov ebp, esp栈帧底部设置完毕后,为局部变量开辟空间,假设开辟一个32(0x20)字节的栈空间。
sub esp, 20h -
执行函数实现的功能;
-
释放局部变量使用的空间:
add esp, 20h -
根据保存的父函数栈帧信息,恢复父函数栈帧:
此时栈顶为父函数的ebp值,可以依据这个信息恢复父函数的ebp,进而恢复栈帧。
pop ebp -
根据保存的返回地址,恢复父函数执行流,一般是函数调用指令后的下一条指令:
当前栈顶为返回地址,这时父函数的栈信息已经恢复,只要根据这个返回地址更改执行流,回到父函数
call func指令的下一条指令即可。retn
至此,一个函数的调用流程结束,栈的状态和调用前完全一致,子函数的返回值被存在eax寄存器中