大家都知道,栈区是存储函数,局部变量的一块内存区域。
那么让我们从汇编的角度,来看看函数的执行过程。首先,当我们使用pushl将数据入栈时,栈顶会移动,以容纳新增加的值。实际上,我们能不断将值入栈,栈会在内存中保持向下增长,知道存放代码或数据的地方。那么,我们如何知道栈顶地址呢?栈寄存器%esp总是包含一个指向当前栈顶的指针。
在执行函数之前,一个程序将函数的所有参数按逆序压入栈中。接着程序发出一条call指令,表明程序希望开始执行哪一个函数。call指令会去做两件事;首先,它将下一条指令的地址即返回地址压入栈中;然后,修改指令指针(%eip)以指向函数起始处。
参数 N |
。。。 |
参数2 |
参数1 |
返回地址 |
现在函数自己也有一些工作。首先通过pushl %ebp指令保存当前基址指针寄存器%ebp。基址指针是一个特殊的寄存器,用于访问函数的参数和局部变量。其次,它会用movl %esp, %ebp。将指针%esp复制到%ebp,这使你能够把函数参数作为相对于基址指针的固定索引进行访问。
在函数开始时将栈指针复制到基址指针寄存器可以让你一直清楚参数的位置(局部变量也是如此),即使在其它数据压入弹出栈的情况下。%ebp将一直是栈指针在函数开始是的位置,所以可以说是对栈帧的常量引用。(栈帧包含一个函数中所使用的所有栈变量,包括参数、局部变量和返回地址。)
参数 N | N×4+4(%ebp) |
。。。 | |
参数2 | 12(%ebp) |
参数1 | 8(%ebp) |
返回地址 | 4(%ebp) |
旧%ebp | (%ebp) 和 (%esp) |
接下来,函数为其所需的所有局部变量保留栈空间,只需将栈指针向外移动即可实现。假设要运行函数,我们需要两个字的内存,只需要将栈指针向下移动两个字即可。
sub $8, %esp
这样,我们久能就爱那个栈用于变量的存储,而不需要担心函数调用引起的入栈会破坏存储的变量。因为函数调用是在栈帧上分配的,而变量仅仅在函数运行期间有效,而当函数返回时,栈帧久不复存在,这些变量也就不存在了。
假如我们有两个字可用于本地存储。那么栈就是如下情况了:
参数 N | N×4+4(%ebp) |
。。。 | |
参数2 | 12(%ebp) |
参数1 | 8(%ebp) |
返回地址 | 4(%ebp) |
旧%ebp | (%ebp) |
局部变量 1 | -4(%ebp) |
局部变量 2 | -8(%ebp) 和 (%esp) |
当一个函数执行完毕后,会做三件事。
(1) 将其返回值存储到%eax。
(2) 将栈恢复到调用函数时的状态(移除当前栈帧,并使调用代码的栈帧重新生效)。
(3) 将控制权换给调用它的程序。这是通过ret指令实现的,该指令将栈顶的值弹出,并将指令指针寄存器%eip设置为该弹出值。
因此,要执行如下命令:
movl %ebp, %esp
popl %ebp
ret
现在控制权已经回到,调用函数的那里,你可以检查%eax中的返回值。弹出其入栈的所有参数,将栈指针复位至其原先的位置(如果不需要参数值,按你可以用addl 将“4 *参数个数”加到%esp即可)。