VC环境下对函数调用的汇编分析【原创】

类别:VC语言 点击:0 评论:0 推荐:
VC环境下对函数调用的汇编分析【原创】   前沿:对于我们平常编程中常出现一些细节,如__stdcall和__cdecl编译器如何为我们处理,函数中变量以及new出来的变量到底存放于哪些地方,等等一些列问题。本文将和大家一起分析程序执行的汇编语言,通过对此过程掌握使自己在开发中熟悉并优化自己的代码。作者:天衣有缝,联系邮件:[email protected],MSN:[email protected],我的QQ群3226292,转载请保留完整文档。   1.环境:我使用的开发环境是vc7.1,其release单步调试需要对项目属性作如下修改: “C++”--》“常规”--》“调试信息格式”  改为:“用于“编辑并继续”的程序数据库(/ZI)” “C++”--》“优化”--》“优化”          改为:禁用(/Od) 如果你是vc6环境,可如下修改release版属性: 选中Win32 Release然后
Project-》setting-》C/C++ -》Category-》General
-》Optimization-》Disable(Debug)
-》Debug Info-》Program DataBase
-》Link---》Generate Debug Info打上钩   2.c语言代码,如下: /***开始*****************************************************/ #include "stdafx.h" int __cdecl add(int a, int b)
{
 int c;
 c = a + b;
 return c;
} int _tmain(int argc, _TCHAR* argv[])
{
 int iResult = add(123, 456);
 printf("\n************\n");    return 0;
} /***结束*****************************************************/   3.程序分析过程,F10单步启动程序: /***开始*****************************************************/
int _tmain(int argc, _TCHAR* argv[])
{
00401020  push        ebp              建立堆栈帧
00401021  mov         ebp,esp          存入栈基地址,运行后EBP = 0012FEE4                                        (表明默认堆栈大小约为1兆)
00401023  sub         esp,44h          空出一块堆栈区,不知道是干什么的,可有高人指点? ××××××××××××××××××××× 在一篇vc6的汇编调试文章中看到main中变量存储于此堆栈中,但是我在vc7调试发现iresult地址根本不在堆栈范围之内,不知作何解释 ×××××××××××××××××××××
00401026  push        ebx              保护现场
00401027  push        esi 
00401028  push        edi 
 int iResult = add(123, 456);
00401029  push        1C8h             参数入栈
0040102E  push        7Bh 
00401030  call        add (401000h)    执行函数调用,按F11跳到本文蓝色处 ××××××××××××××××××××× call指令详解: call指令是push和jmp的结合,先执行push eip将当前地址入栈(函数调用完毕需要用这个地址返回),然后调用jmp指令。因为普通指令无法对eip操作,所以在很多病毒程序中常有如下语句:        call            @@get_eip
@@get_eip:  
       pop  ebp ;取得eip ×××××××××××××××××××××
00401035  add         esp,8            释放123和456变量所占堆栈
00401038  mov         dword ptr [iResult],eax           从eax取出计算结果
 printf("\n************\n");           下面是一个函数的测试
0040103B  push        offset string "\n************\n" (4060FCh)        变量地址入栈
00401040  call        printf (401051h) 执行call调用函数
00401045  add         esp,4            变量地址出栈    return 0;
00401048  xor         eax,eax          使eax为0,eax就是返回给操作系统的值
}
0040104A  pop         edi 
0040104B  pop         esi 
0040104C  pop         ebx              恢复现场
0040104D  mov         esp,ebp          平衡堆栈
0040104F  pop         ebp              释放堆栈帧
00401050  ret                          返回操作系统调用处   函数定义:  int __cdecl add(int a, int b)
{
00401000  push        ebp         建立堆栈帧
00401001  mov         ebp,esp     存入栈基地址
00401003  sub         esp,44h     开辟变量使用的堆栈区,供函数内部变量使用                                   执行前ESP = 0012FE84,执行后ESP = 0012FE40 ××××××××××××××××××××× 此处可以打开内存0x0012FE8C,看到 7b 00 00 00 c8 01 00 00,这就是我们传入的123(0x0012fe8c处)和456(0x0012fe90处)变量
×××××××××××××××××××××
00401006  push        ebx         保护现场
00401007  push        esi 
00401008  push        edi 
 int c;
 c = a + b;
00401009  mov         eax,dword ptr [a]   第一个参数,也就是[ebp+8]
0040100C  add         eax,dword ptr [b]   第二个参数,也就是[ebp+c]

0040100F  mov         dword ptr [c],eax   c变量在栈中,地址为0x0012fe80,就是变量堆栈区顶部
 return c;
00401012  mov         eax,dword ptr [c]   计算结果存入eax
}
00401015  pop         edi        回复现场
00401016  pop         esi 
00401017  pop         ebx 
00401018  mov         esp,ebp    平衡堆栈,回收变量堆栈区
0040101A  pop         ebp        释放堆栈帧
0040101B  ret                    回到调用地址,读者从这里转到粉红色处接着看 /***结束*****************************************************/     4.关于__cdecl和__stdcall:(vc项目默认调用方式为__cdecl) 我们将上面的add函数改为__stdcall形式,执行过程如下,我在和上面__cdecl调用不同的地方将作出标记: /***开始*****************************************************/ int _tmain(int argc, _TCHAR* argv[])
{
00401020  push        ebp 
00401021  mov         ebp,esp
00401023  sub         esp,44h
00401026  push        ebx 
00401027  push        esi 
00401028  push        edi 
 int iResult = add(123, 456);
00401029  push        1C8h
0040102E  push        7Bh 
00401030  call        add (401000h) 注意,这句话后面没有了add  esp,8,原因:__stdcall调用方式的入栈参数在函数内部已经释放了,所以这句话也就不需要了。
00401035  mov         dword ptr [iResult],eax
 printf("\n************\n");
00401038  push        offset string "\n************\n" (4060FCh)
0040103D  call        printf (40104Eh)
00401042  add         esp,4    return 0;
00401045  xor         eax,eax
}
00401047  pop         edi 
00401048  pop         esi 
00401049  pop         ebx 
0040104A  mov         esp,ebp
0040104C  pop         ebp 
0040104D  ret        函数部分:   int __stdcall add(int a, int b)
{
00401000  push        ebp 
00401001  mov         ebp,esp
00401003  sub         esp,44h
00401006  push        ebx 
00401007  push        esi 
00401008  push        edi 
 int c;
 c = a + b;
00401009  mov         eax,dword ptr [a]
0040100C  add         eax,dword ptr [b]
0040100F  mov         dword ptr [c],eax
 return c;
00401012  mov         eax,dword ptr [c]
}
00401015  pop         edi 
00401016  pop         esi 
00401017  pop         ebx 
00401018  mov         esp,ebp
0040101A  pop         ebp 
0040101B  ret         8    这句话就是__stdcall调用方式的入栈参数 在函数内部释放的语句! /***结束*****************************************************/ 说明:对于函数的传值还是传址,大家在此之后自行分析,相信初学者可以看到很多细节方面的咚咚   5.全局变量的初始化: #include "stdafx.h" const char szName[]= "http://blog.csdn.net/waterpub";
class CTestClass
{
public:
 CTestClass()
 {
  printf("CTestClass::CTestClass()\n");
 }
 ~CTestClass()
 {
  printf("CTestClass::~CTestClass()\n");
 }
}; const CTestClass tobject; int _tmain(int argc, _TCHAR* argv[])
{
 printf("\n************\n");
 return 0;
}   在这个程序中szname如何初始化的,为何没有执行到对应的反汇编语句? 因为main函数开始执行的时候,szname已经初始化了,所有我们运行不到这个地方。当用户启动这个exe程序的时候,进入C/C++ 运行时库代码(CRTStartup),由它初始化静态变量及全局变量,然后再转入main函数。   我们现在在上面绿色部分设置一个断点,然后运行,程序断在此处。按(Ctrl+Alt+C :VC7.1的快捷键,vc6有相应的菜单项),点到最下面调用函数,从此可以看出:程序启动时由操作系统执行“mainCRTStartup”函数,简化的代码我贴在下面了,一些很直观的英文没有翻译: /***开始*****************************************************/
int WinMainCRTStartup(void)   {
 int initret;
 int mainret;
 OSVERSIONINFOA *posvi;
 int managedapp;
 posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));//用这个函数避免了全局存储缓冲区的分配运行时检测    //操作系统版本相关的一些代码
 posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
 (void)GetVersionExA(posvi);
 _osplatform = posvi->dwPlatformId;
 _winmajor = posvi->dwMajorVersion;
 _winminor = posvi->dwMinorVersion;
 _osver = (posvi->dwBuildNumber) & 0x07fff;
 if ( _osplatform != VER_PLATFORM_WIN32_NT )
  _osver |= 0x08000;
 _winver = (_winmajor << 8) + _winminor;
 //是否为托管程序
 managedapp = check_managed_app();   #ifdef _MT
 if ( !_heap_init(1) )               /* 多线程用此方式初始化堆 */
#else  /* _MT */
 if ( !_heap_init(0) )               /* 单线程用此方式初始化堆 */
#endif  /* _MT */
  fast_error_exit(_RT_HEAPINIT);  /* write message and die */   #ifdef _MT
 if( !_mtinit() )                    /* initialize multi-thread */
  fast_error_exit(_RT_THREAD);    /* write message and die */
#endif  /* _MT */    /*
 * Initialize the Runtime Checks stuff
 */
#ifdef _RTC
 _RTC_Initialize();                  // 初始化runtime
#endif  /* _RTC */
 /*
 * 下面是剩余的初始化代码(包括我的程序的全局变量的初始化就在这里了,呵呵)
 * 初始化完毕调用 main 或  WinMain(在try里面执行的)
 */    __try {     if ( _ioinit() < 0 )            /* io初始化,应该是输入输出吧,不太清楚 */
   _amsg_exit(_RT_LOWIOINIT);   #ifdef WPRFLAG
  /* get wide cmd line info */
  _wcmdln = (wchar_t *)__crtGetCommandLineW();           //取得运行参数的字符串     /* get wide environ info */
  _wenvptr = (wchar_t *)__crtGetEnvironmentStringsW();   //取得环境变量的字符串     if ( _wsetargv() < 0 )
   _amsg_exit(_RT_SPACEARG);
  if ( _wsetenvp() < 0 )
   _amsg_exit(_RT_SPACEENV);
#else  /* WPRFLAG */
  /* get cmd line info */
  _acmdln = (char *)GetCommandLineA();     /* get environ info */
  _aenvptr = (char *)__crtGetEnvironmentStringsA();     if ( _setargv() < 0 )
   _amsg_exit(_RT_SPACEARG);
  if ( _setenvp() < 0 )
   _amsg_exit(_RT_SPACEENV);
#endif  /* WPRFLAG */     initret = _cinit(TRUE);                  /* 全局变量初始化,找的就是这里!!! */
  if (initret != 0)
   _amsg_exit(initret);   #ifdef _WINMAIN_     StartupInfo.dwFlags = 0;
  GetStartupInfo( &StartupInfo );   #ifdef WPRFLAG
  lpszCommandLine = _wwincmdln();
  mainret = wWinMain(
#else  /* WPRFLAG */
  lpszCommandLine = _wincmdln();
  mainret = WinMain(
#endif  /* WPRFLAG */
   GetModuleHandleA(NULL),
   NULL,
   lpszCommandLine,
   StartupInfo.dwFlags & STARTF_USESHOWWINDOW
   ? StartupInfo.wShowWindow
   : SW_SHOWDEFAULT
   );
#else  /* _WINMAIN_ */   #ifdef WPRFLAG
  __winitenv = _wenviron;
  mainret = wmain(__argc, __wargv, _wenviron);
#else  /* WPRFLAG */
  __initenv = _environ;
  mainret = main(__argc, __argv, _environ);       
  // 就在这里调用了main,因为运行时代码在exe文件中,所以可以把main函数拿来调用(跟普通的函数没什么区别了,如果你看了win32汇编就知道main或winmain名字都不是定死的了)!
#endif  /* WPRFLAG */   #endif  /* _WINMAIN_ */     if ( !managedapp )
   exit(mainret);     _cexit();    }
 __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )   //  异常就到这里来了,比如丢失了dll文件
 {
  /*
  * Should never reach here
  */     mainret = GetExceptionCode();     if ( !managedapp )
   _exit(mainret);     _c_exit();    } /* end of try - except */    return mainret;
} /***结束*****************************************************/      6.win32的启动过程 既然console的程序我们分析出来了,win32的又有什么区别呢?区别还是有的(启动程序的核心代码都在crt0.c文件中),上面我把具体的分析方法阐述了一下,win32的分析就留给大家做好啦,:)    7.错误可能也有的,或者可以写的更好,但本菜也只有这个水平了,贻笑大方,悉请高手不吝指正。文章可能随时修改,如果你有什么问题或好的想法,到我的blog(http://blog.csdn.net/waterpub)上留言,不要在这里留了,我来的少,:)    8.深圳南山科技园科技工业大厦       2005-02-22  17:00:00                                

本文地址:http://com.8s8s.com/it/it228.htm