Coception:
栈:堆栈(stack)又称为栈或堆叠,是计算机科学里最重要且最基础的数据结构之一,它按照FILO(First In Last Out,后进先出)的原则存储数据。
栈顶:元素插入(压栈PUSH,减4或8)和删除(出栈POP,加4或8)的地方。栈底:另一端。
从技术上说,栈就是CPU寄存器里的某个指针所指向的一片内存区域。这里所说的“某个指针”通常位于x86/x64平台的ESP寄存器/RSP寄存器,以及ARM平台的SP寄存器。作用如下, 暂时保存函数内的局部变量。调用函数时传递参数。保存函数返回的地址。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。简言之,栈帧就是利用EBP(栈帧指针,请注意不是ESP)寄存器访问局部变量、参数、函数返回地址等的手段。
;栈帧结构
PUSH EBP ;函数开始(使用EBP前先把已有值保存到栈中)
MOV EBP, ESP ;保存当前ESP到EBP中
... ;函数体
;无论ESP值如何变化,EBP都保持不变,可以安全访问函数的局部变量、参数
MOV ESP, EBP ;将函数的起始地址返回到ESP中
POP EBP ;函数返回前弹出保存在栈中的值
RETN ;函数终止
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 函数调用的上下文
栈是从高地址向低地址延伸,一个函数的栈帧用EBP和ESP这两个寄存器来划定范围。EBP指向当前栈帧的底部,ESP始终指向栈帧的顶部。
EBP寄存器又被称为帧指针(Frame Pointer)
ESP寄存器又被称为栈指针(Stack Pointer)
经典代码如下:
#include <stdio.h>
int foo(int x, int y)
{
int z;
z = x + y;
return z;
}
int main(int argc, char* argv[])
{
int a;
a = foo(5, 6);
printf("a = %d", a);
}
main函数调用了foo函数,所以main函数又称调用函数(caller),foo函数又被称为被调用函数(callee)。C使用的调用惯例是cdec1。
Tips: 常用调用惯例
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数,如函数int func(int a,double b)的修饰名是_func@12 |
fastcall | 函数本身 | 头两个DWORD(4字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈 | @+函数名+@+参数的字节数 |
pascal | 函数本身 | 从左至右的顺序压参数入栈 | 较为复杂,参加pascal文档 |
先y后z,push eip(保存返回地址)
=>保存调用函数的ebp(也就是caller的基地址),然后将ebp寄存器的值更新为当前的栈顶的地址(也就是ebp被更新为callee的基地址)。接下来通过sub指令来抬高栈顶,为了保存寄存器和局部变量。然后通过ebp+偏移的方式将参数保存到寄存器中,然后进行相加,将结果保存到eax寄存器中,在32位程序中,返回值是放在eax寄存器里的。
最后ret,第一步pop 返回地址,第二步jmp 返回地址。
函数具体的调用过程就是利用栈帧保存临时变量(保存返回地址)和返回地址(跳转函数)。