Win32程序函数调用时堆栈变化情况

Win32程序函数调用时堆栈变化情况分析   
            在经典的汇编语言教程中,函数调用时堆栈的使用都是着重讲解的问题。如今随着高级语言的越来越完善,单纯使用汇编开发的程序已经不多了。但对函数调用时堆栈动向的了解仍有助于我们明晰程序的执行流程,从而在程序编写和调试的过程中有一个清晰的思路。  
   
  一.调用约定  
  在Win32中,有关函数的调用主要有两种约定。  
  1._stdcall  
                  以__stdcall方式调用的函数有以下特征:  
          •     参数由右至左压栈  
          •   调用返回时,堆栈由被调函数调整  
  2.__cdecl  
  __cdecl约定是C/C++函数的默认调用约定。它有以下特征:  
          •   参数由右至左压栈  
          •   调用返回时,堆栈由调用者调整  
   
  二.Win32函数调用过程  
   
  1.         压入参数  
  这里依据以上的调用方式将调用者给出的参数一一压入堆栈。  
   
  2.         压入断点  
  当程序执行到Call指令的时候,当前语句的地址作为断点地址压入堆栈。  
   
  3.         跳转  
  eip的值被重新设置为被调函数的起始地址。  
   
  4.         mov   ebp,   esp  
  这里ebp被用来在堆栈中寻找调用者压入的参数,同时作为调用者堆栈指针的一个备份。在此前还应该执行一条:  
  push   ebp  
  把ebp中原来的数值保存。  
   
  5.         sub   esp,N  
  这里N是函数内局部变量的总字节数加上一个整数,一般为40。此后esp即为被调函数的堆栈指针了。  
   
  6.         初始化esp   ~   esp-N之间的N字节空间  
  这是对堆栈中已分配给局部变量使用的内存空间的初始化,一般全部设置为0xcc。  
   
  7.         顺序执行函数内语句。  
  此时函数的堆栈位于所有局部变量的内存空间之后,二者之间一般有40字节的隔离带。  
   
  8.返回  
  为保障调用的正常返回,函数内应当保证规范使用堆栈,使即将返回的时候esp的值恢复为执行第一条语句前的状态。说明白点,就是每一条push都要有相应的pop。  
  调用返回的过程如下:  
  mov   esp,   ebp  
  执行后,esp恢复为调用者的堆栈指针,栈顶除断点地址外,还存有原ebp的值和调用时压入的参数。  
  然后依次弹出ebp的值和断点地址。如果是__cdecl约定则直接返回调用者,调用者将负责调整堆栈,丢弃调先前压入的参数。如果是__stdcall则这个工作由被调函数来执行。  
   
  程序样例如下:  
  ……  
  0040B8E8       push                 1                         ;压入参数  
  0040B8EA       call                 00401028                 ;调用函数  
  ……  
  00401028       jmp                   0040b7c0                 ;跳转到函数入口  
  ……  
  0040B7C0       push                 ebp                         ;保存ebp  
  0040B7C1       mov                   ebp,esp                  
  0040B7C3       sub                   esp,44h                 ;设置函数的堆栈指针,此函数中有4  
  ;字节的局部变量  
  0040B7C6       push                 ebx  
  0040B7C7       push                 esi                  
  0040B7C8       push                 edi  
  0040B7C9       lea                   edi,[ebp-44h]          
  0040B7CC       mov                   ecx,11h  
  0040B7D1       mov                   eax,0CCCCCCCCh  
  0040B7D6       rep   stos         dword   ptr   [edi]         ;初始化局部变量空间  
  0040B7D8       mov                   eax,dword   ptr   [ebp+8]  
  0040B7DB       mov                   dword   ptr   [ebp-4],eax  
  ……  
  0040B7DE       pop                   edi                         ;弹出曾压栈的数据  
  0040B7DF       pop                   esi  
  0040B7E0       pop                   ebx  
  0040B7E1       mov                   esp,ebp                 ;恢复调用者的堆栈  
  0040B7E3       pop                   ebp                         ;弹出原ebp值  
  0040B7E4       ret                   4                         ;返回并将堆栈向上调整4字节。  
  ;此处为__stdcall约定,所以由函数调  
  ;整堆栈  
   
  相应的C代码如下:  
   
  void   __stdcall   fun(int);  
   
  int   main(void)  
  {  
          ……          
  fun(1);  
  ……  
          return   0;  
  }  
   
  void   __stdcall   fun(int   para)  
  {  
          int   localpara   =   para;  
          ……  
  }  
     
   
   
  C   与C++的两种调用方法  
  (1)_stdcall:用于Win32   API函数和COM+接口。它从右向左将参数推入堆栈,被调函数在返回之前从堆栈中弹出自己的参数。从堆栈中弹出自己参数的函数不支持参数数目的变化。  
  (2)_cdecl:C和C++默认方式)。它也是从右向左传递参数。但是被调函数不负责从堆栈中弹出参数,调用函数将在函数调用返回后晴空堆栈。  
  CALLBACK采用方式(1)。它称为回调函数,即供系统调用的函数。  
  例如窗口函数、定时处理函数、线程处理函数等。  
   
  CALLBACK   是老式叫法,现在微软都改为WINAPI.  
   
   
   
  至于编译器的解释上。c与c++搞的很麻烦。  
   
  我也记不住每次碰到都去查一下.  
   
  我列个表:  
   
  语言                                     调用约定                         输出方式  
   
  c                                           stdcall                     _SomeFunctionname@Number  
   
   
  c                                           cdecl                         _SomeFunctionname  
       
   
   
   
  C++的烦的要死。  
  C++编译时函数名修饰约定规则:    
   
  __stdcall调用约定:    
  1、以“?”标识函数名的开始,后跟函数名;    
  2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;    
  3、参数表以代号表示:    
  X--void   ,    
  D--char,    
  E--unsigned   char,    
  F--short,    
  H--int,    
  I--unsigned   int,    
  J--long,    
  K--unsigned   long,    
  M--float,    
  N--double,    
  _N--bool,    
  ....    
  PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;    
  4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;    
  5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。    
   
  其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如    
  int   Test1(char   *var1,unsigned   long)-----“?Test1@@YGHPADK@Z”    
  void   Test2()   -----“?Test2@@YGXXZ”    
   
  __cdecl调用约定:    
  规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。    
   
  __fastcall调用约定:    
  规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。    
               
   
  编写DLL的时候输出函数老是不认就是编译器解释的不同造成的。EXTERN   "C"就是为了统一。全部用c方式解释。  
   
   
   
  两种声明是不同的,vc默认的调用是__cdecl   ,所以   extern   "C"   __declspec(dllimport)   int   mydllfunc(int);是extern   "C"   __declspec(dllimport)   int   __cdecl   mydllfunc(int);  
  因此和extern   "C"   __declspec(dllimport)   int   __stdcall   mydllfunc(int);是有区别的    

原文地址:https://www.cnblogs.com/hellohuan/p/1237371.html