一、栈知识
栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现。在我们的例子中, 堆栈是向下增长的。堆栈指针(SP)也是依赖于具体实现的。它可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址。在我们的讨论当中, SP指向堆栈的最后地址。
栈从高地址向低地址增长,栈底为一个固定的低地址。栈由栈帧(stack frame)组成,当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑堆栈帧被从栈中弹出。栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据, 其中包括在函数调用时指令指针(IP)的值。
SP(stack pointer): 栈指针,指向栈顶。
FP(frame pointer):帧指针,指向帧内固定地址的指针。也叫局部基指针(LB-local base pointer)。
EBP(extended base pointer):它包含了帧指针
二、调用栈
当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复)。 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间,这称为例程的序幕(prolog)工作。当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作。
一个简单的例子
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
通过查看汇编语言输出, 我们看到对function()的调用被翻译成:
pushl $3
pushl $2
pushl $1
call function
以从后往前的顺序将function的三个参数压入栈中, 然后调用function()。 指令CALL会把指令指针(EIP)也压入栈中,我们把这被保存的IP称为返回地址(RET),而在执行RET指令时,IP重新被弹入至EIP寄存器中,继续执行下一条指令。在函数中所做的第一件事情是例程的序幕工作:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
将帧指针EBP压入栈中。 然后把当前的SP复制到EBP, 使其成为新的帧指针。 我们把这个被保存的FP叫做SFP。 接下来将SP的值减小, 为局部变量保留空间。
内存只能以字为单位寻址。 一个字是4个字节, 32位。因此5字节的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间。这就是为什么SP要减掉20的原因。这样我们就可以想象function()被调用时堆栈的模样(每个空格代表一个字节):
内存低地址 内存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆栈顶部 堆栈底部