开始

x86下的栈结构
文字叙述流程
- 参数入栈。
push arg0
- 按照调用约定逐个入栈,例如
cdecl
是参数从右向左入栈。 esp
指针按照入栈次数减少值。
- 按照调用约定逐个入栈,例如
- 返回地址入栈。
esp
减少call xxx
- 函数调用,进入函数内部。
- 保存
ebp
寄存器到栈顶。push ebp
esp
是全局的栈顶指针,为了在局部开辟栈帧空间,我们需要一个基准点,这个基准点就是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
个字节,这是一种编译器优化,用更短的指令替换。