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的分析就留给大家做好啦,:)
8.深圳南山科技园科技工业大厦 2005-02-22 17:00:00

|