VC环境下对函数调用的汇编分析【原创】
前沿:对于我们平常编程中常出现一些细节,如__stdcall和__cdecl编译器如何为我们处理,函数中变量以及new出来的变量到底存放于哪些地方,等等一些列问题。本文将和大家一起分析程序执行的汇编语言,通过对此过程掌握使自己在开发中熟悉并优化自己的代码。作者:天衣有缝,联系邮件:waterpub@mail.csdn.net,MSN:waterpub_cn@hotmail.com,我的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