调用惯例
函数调用方和被调用方对于函数调用必须要有一个约定(传递参数和返回值以及函数本身如何使用堆栈),只有这样函数才可以被正确调用,这样的约定称为调用惯例。
参数压入栈的顺序
出栈的控制权
默认调用惯例是出栈方是主调函数,参数从右到左入栈
栈帧
多数栈是逆增长的,也就是向低地址增长,函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
栈帧的大小?
栈帧中函数局部变量布局方式?
函数的调用栈原理
调用栈由汇编语言通过 push 和 pop 指令来控制,esp 是栈顶指针,ebp 是帧指针或基址指针(通过ebp来访问函数的局部变量,ebp增加是参数,减少是函数内变量)
ADD(1,3);
ADD(int x, int y) {
int i = 5;
int j = 7;
int k = 9;
return x + y;
}
// 汇编
push 3
push 1
call ADD
add esp,8 ;ReturnAddr // 由主调函数负责参数的入栈出栈
-------------
ADD:
push ebp
mov ebp esp
sub esp,0xc
mov dword ptr ss:[ebp-4],5
mov dword ptr ss:[ebp-8],7
mov dword ptr ss:[ebp-0xc],9
mov eax,dword ptr ss::[ebp+8]
mov eax,dword ptr ss::[ebp+0xc]
mov esp,ebp
pop ebp
ret
执行到 mov ebp esp 时栈内状态
sub esp,0xc 由于函数内 3 个局部变量占用了 12 个字节,所以将 esp 减 0xc
mov dword ptr ss:[ebp-4],5 通过 ebp 给栈中空间赋值,通过 ebp 来访问变量(基址地址)
mov esp,ebp
pop ebp 由于存在多个栈嵌套,所以需要将 ebp 复原,并且将 esp - 4
ret 相当于 pop eip 将 retuenAddr 赋值给 eip,最后通过 add esp,8 释放掉栈空间
小结
一个栈帧包含了参数,eip 地址(函数的上下文返回地址),原始基址,局部变量。调用完一个函数是会产生副作用例如上面的 eax 寄存器记录了函数返回值。
现在我们可以回答上面的两个问题
- 栈帧的大小?
栈帧的大小在编译后就可以确认了,函数局部变量大小+参数大小+4+4。
- 栈帧中函数局部变量布局方式?
栈帧内同样存在着内存对齐,编译器会对编译后生成的汇编语言进行优化,来更高效地读写内存。
局部变量以何种方式布局并未规定。编译器计算函数局部变量所需要的空间总数,并确定这些变量存储在寄存器上还是分配在程序栈上(甚至被优化掉)——某些处理器并没有堆栈。局部变量的空间分配与主调函数和被调函数无关,仅仅从函数源代码上无法确定该函数的局部变量分布情况。
基于不同的编译器版本、优化级别、目标处理器架构、栈安全性等,相邻定义的两个变量在内存位置上可能相邻,也可能不相邻,前后关系也不固定。若要确保两个对象在内存上相邻且前后关系固定,可使用结构体或数组定义。
由主调函数负责参数的入栈出栈,并且负责 eip 指针的跳转,call 指令会自动记录 eip 寄存器的值。