堆栈平衡指的是在调用子程序时,保证子程序执行前后 esp(栈顶指针寄存器)和 ebp(存取堆栈指针)的数据保持一致,把栈恢复到调用前的状态,以保证程序后续能够持续正确运行。

在调用约定里,push 用于保护现场(记录当前栈指向的位置),而 pop 则用来恢复现场。

示例:源代码与反汇编

下面是一个最小的 C++ 示例,以及它在 Debug 模式下的反汇编输出,重点观察函数调用前后 esp / ebp 的变化。

源代码

/*源代码.cpp*/
                
                int  a(int aa,int v1)
                {
                
                		return 5;
                }
                
                int main()
                {
                	int x = 5;
                	int t=a(5, 4);
                
                	return 0;
                }

main 函数的反汇编

/*    debug 反汇编         */
                
                --- c:\users\hekun\desktop\cppp\cppp\源.cpp -------------------------------------
                int main()
                {
                00213AB0  push        ebp
                00213AB1  mov         ebp,esp
                00213AB3  sub         esp,0D8h
                00213AB9  push        ebx
                00213ABA  push        esi
                00213ABB  push        edi
                00213ABC  lea         edi,[ebp-0D8h]
                00213AC2  mov         ecx,36h
                00213AC7  mov         eax,0CCCCCCCCh
                00213ACC  rep stos    dword ptr es:[edi]
                	int x = 5;
                00213ACE  mov         dword ptr [x],5
                	int t=a(5, 4);
                00213AD5  push        4 /*第二参数入栈*/
                00213AD7  push        5  /*第一参数入栈*/
                00213AD9  call        a (021142Eh)   /*调用a*/
                00213ADE  add         esp,8  /*  esp 恢复正确位置,参数占用8字节,  为__cdecl 调用 (调用者 堆栈平衡) */
                00213AE1  mov         dword ptr [t],eax  /*函数返回值位于eax寄存器内*/
                
                	return 0;
                00213AE4  xor         eax,eax  /*清空eax*/
                }
                
                00213AE6  pop         edi
                00213AE7  pop         esi
                00213AE8  pop         ebx
                00213AE9  add         esp,0D8h
                00213AEF  cmp         ebp,esp
                00213AF1  call        __RTC_CheckEsp (02112DFh)
                00213AF6  mov         esp,ebp
                00213AF8  pop         ebp
                00213AF9  ret

函数 a 内的反汇编

/*函数a内 反汇编*/
                
                --- c:\users\hekun\desktop\cppp\cppp\源.cpp -------------------------------------
                #include "iostream"
                using namespace std;
                
                int  a(int aa,int v1)
                {
                013241A0  push        ebp  /*保护缺点ebp 状态*/
                013241A1  mov         ebp,esp  /*ebp指向栈顶(esp 指向 变量aa )*/
                013241A3  sub         esp,0C0h  /*预留 192 字节空间*/
                013241A9  push        ebx
                013241AA  push        esi
                013241AB  push        edi
                013241AC  lea         edi,[ebp-0C0h]
                013241B2  mov         ecx,30h
                013241B7  mov         eax,0CCCCCCCCh
                013241BC  rep stos    dword ptr es:[edi]
                
                		return 5;
                013241BE  mov         eax,5  /*返回值保存eax寄存器中*/
                }
                013241C3  pop         edi
                013241C4  pop         esi
                013241C5  pop         ebx
                013241C6  mov         esp,ebp  /恢复现场  恢复函数a执行前的 栈顶 (esp)*/
                013241C8  pop         ebp
                013241C9  ret

要点小结

  • 调用方通过连续的 push 把参数从右向左压栈,再 call 进入子程序。
  • 子程序入口会 push ebp / mov ebp, esp 建立新的栈帧,并用 sub esp, XXh 为局部变量预留空间。
  • 子程序退出前通过 mov esp, ebp / pop ebp 恢复到进入时的栈顶状态,返回值通过 eax 带出。
  • 对于 __cdecl 调用约定,参数占用的栈空间由调用者负责清理,也就是示例中 call a 之后的 add esp,8,对应两个 4 字节参数一共 8 字节。这就是"调用者堆栈平衡"的由来。