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@12fastcall函数本身头两个DWORD(4字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈@+函数名+@+参数的字节数pascal函数本身从左至右的顺序压参数入栈较为复杂,参加pascal文档先y后z,push eip(保存返回地址) =>保存调用函数的ebp(也就是caller的基地址),然后将ebp寄存器的值更新为当前的栈顶的地址(也就是ebp被更新为callee的基地址)。接下来通过sub指令来抬高栈顶,为了保存寄存器和局部变量。然后通过ebp+偏移的方式将参数保存到寄存器中,然后进行相加,将结果保存到eax寄存器中,在32位程序中,返回值是放在eax寄存器里的。
最后ret,第一步pop 返回地址,第二步jmp 返回地址。
函数具体的调用过程就是利用栈帧保存临时变量(保存返回地址)和返回地址(跳转函数)。