开始
x86下的栈结构
文字叙述流程
- 参数入栈。
push arg0- 按照调用约定逐个入栈,例如
cdecl是参数从右向左入栈。 esp指针按照入栈次数减少值。
- 按照调用约定逐个入栈,例如
- 返回地址入栈。
esp减少call xxx - 函数调用,进入函数内部。
- 保存
ebp寄存器到栈顶。push ebpesp是全局的栈顶指针,为了在局部开辟栈帧空间,我们需要一个基准点,这个基准点就是ebp也被称为局部栈底指针。- 保存
ebp之后,我们便可以使用ebp来描述局部栈空间,例如[ebp - 4]表示一个局部变量。
- 赋值
ebp到当前局部栈的栈底。mov ebp, esp - 开辟局部变量的空间。
sub esp, 32h- 此时局部变量1:
[ebp - 4],局部变量2:[ebp - 8] - 此时参数1:
[ebp + 8], 参数2:[ebp + 12] - 返回地址:
[ebp + 4] - 原始的
ebp值:[ebp]
- 此时局部变量1:
- 保存寄存器环境(可选)。
- 在
vc6_x86的debug版本中,将会固定入栈12个字节的寄存器环境。
- 在
- 执行函数代码,直到函数将要结束。
- 寄存器环境出栈(可选),此时栈顶为
ebp原本的值。- 注意出栈顺序和入栈顺序相反
- 恢复
ebp值。pop ebp- 此举可以将
ebp栈底指针恢复,不再计算局部变量的空间。
- 此举可以将
- 返回
ret。出栈return_address - 清理参数导致的堆栈移动痕迹。
add esp, 0Ch
结合代码
函数调用中栈的结构分两部分讨论:
- 函数外部
- 函数内部
函数外部
// foo函数签名int foo(int a, uint64_t b);
int main(){ // 进行函数调用 foo(1, 2l);}我们输入到godbolt网站中,查看汇编代码:
_main PROC ; COMDAT ; 参数入栈 push 0 push 2 push 1 ; 参数入栈结束 call int foo(int,unsigned __int64) ; 返回地址入栈 add esp, 0Ch ; 清理参数导致的堆栈移动痕迹。 xor eax, eax ret 0_main ENDP在函数调用的外部,我们可以看到符合我们的文字描述,进行了:1、2、3、12这几步。
函数内部
接下来我们关注函数内部的部分:
为了不被优化,这里使用Debug版本的编译。
msvc v19
int foo(int a, uint64_t b){ int val = a + b; printf("aaa"); return 0;}汇编代码
_val$ = -4 ; 可以看到这里已经标注了_val的偏移是[ebp-4]a$ = 8 ; size = 4_b$ = 12 ; size = 8?foo@@YAHH_K@Z PROC push ebp ; 保存ebp值 mov ebp, esp ; ebp指向局部栈 push ecx ; 不是保存寄存器环境 而是将esp - 4开辟了空间 mov eax, DWORD PTR _a$[ebp] cdq add eax, DWORD PTR _b$[ebp] mov DWORD PTR _val$[ebp], eax push OFFSET $SG33737 call _printf add esp, 4 xor eax, eax mov esp, ebp pop ebp ; 恢复ebp值 esp+=4 ret 0 ; 跳转到返回地址 esb+=4?foo@@YAHH_K@Z ENDP这里面需要注意的是push ecx这一条指令,一般来说这都是保存寄存器环境,然后在函数内部使用ecx寄存器,最后将ecx还原。
但是这里我们没有看到还原的过程,且ecx是一个易失寄存器,一般来说不需要保存环境。
所以这里的作用主要是将esp推4个字节,这是一种编译器优化,用更短的指令替换。