导航

__stdcall与__cdecl之区别浅析及相关知识

Posted on 2012-10-25 20:23  codeape  阅读(439)  评论(0编辑  收藏  举报

写完后有人说像教程啊有木有,算了,懒得改了,就这样吧。

1)        进程地址空间布局简介

 

可见栈是由高地址向低地址方向增长。

 

2)        __stdcall__cdecl调用方式的区别

__stdcall是Pascal程序的缺省调用方式,通常用于Win32 API中,函数采用从右到左的压栈方式,自己在退出时清空栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。 int f(void *p) -->> _f@4(在外部汇编语言里可以用这个名字引用这个函数)。

C调用约定(即用__cdecl关键字说明)(The C default calling convention)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数 vararg的函数(如printf)只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。__cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空栈的代码,所以产生的可执行文件大小会比调用__stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。

3)        细观__stdcall__cdecl

__stdcall方式调用函数,此刻参数已经入栈,ESP = 0x22FF68。

下面跟进函数内部,

略过中间无关过程,函数即将返回,

返回地址出栈后,ESP = 0x22FF68。0x22FF68 + 0xC = 0x22FF74。

函数调用结束恢复函数调用前(参数入栈之前)的ESP,抬高栈顶指针,为下一个调用做好准备。

下面是__cdecl:

一个__cdecl方式调用的开始,参数入栈完毕,ESP = 0x22FF60。

函数即将返回,ESP = 0x22FF5C,函数返回ESP = 0x22FF5C + 4 = 0x22FF60。可见被调用函数未对栈进行恢复。

4)        程序功能简介

程序的主要功能是根据指定PID获取进程主模块的映像文件全路径。程序会用到一个至关重要的API-GetModuleFileNameEx,使用gcc链接时提示找不到该函数的定义,于是采用LoadLibrary("psapi.dll");-->GetProcAddress(hPSAPI, "GetModuleFileNameExA");的方式获取函数地址。首先看一下MSDN关于GetModuleFileNameExA的定义,

看了MSDN的文档后,遂信心满满地开始定义函数指针:

typedef unsigned long (*MY_GET_MODULE_FILE_NAME_EX)(HANDLE hProcess, HMODULE hModule, char *lpFilename, unsigned long nSize);

接着获取函数地址:

MyGetModuleFileNameEx = (MY_GET_MODULE_FILE_NAME_EX)GetProcAddress(hPSAPI, "GetModuleFileNameExA");

函数调用:

编译链接通过,欣喜若狂。

5)        运行异常现象

信心满满地运行程序,

好吧,多么友好的错误提示。调试发现,每次调用GetModuleFileNameExA时都崩溃,好吧,微软你又坑我,上次使用版本信息的API时你单方面try:catch的事我就不提了。继续反复调试,排除其他所有可能,更加坚定了微软再次出问题的想法。期间与组里某牛人交流的时候,牛人问是不是崩在微软的模块。上次那个问题确实是崩在了微软的一个DLL里面了,具体哪个记不清了,这次好像不太一样啊,崩在自己模块了啊,不过在找到问题原因之前一直坚信是微软的问题。

后来经某牛人指导,终于找到了问题的原因。具体什么原因先不说,先看一下正常的程序和异常的程序到底哪里不一样吧。

6)        反汇编分析源程序

程序源码:

经过部分修改的错误程序,崩溃点在char ImgFileName[MAX_PATH] = {0};。

反汇编运行正确的程序,找到源码对应的位置:

“rep stos byte ptr es:[edi]”对应” char ImgFileName[MAX_PATH] = {0};”;

ESI内存储了GetModuleFileNameExA的地址。

下面是运行崩溃的程序:

注意途中框出的位置,问题的根源就在这里。可以看出,崩溃的程序是以“__cdecl”方式调用的GetModuleFileNameExA。

rep stos byte ptr es:[edi]指令的操作是对以EDI存储的地址为起始地址长度为[ECX]的内存区域进行赋0操作。EDI的值来源于[EBP-260]。之前提到崩溃的位置就是rep stos byte ptr es:[edi],下面就具体观察[EBP-260]的值。

在正常的程序中,[EBP-260]――也就是栈帧底指针向上偏移0x260的值(也就是”ImgFileName”的起始地址)在循环中是不变的:

再观察异常程序的[EBP-260]:

第一次调用GetModuleFileNameExA前后[EBP-260]均正常,当GetModuleFileNameExA后续的若干函数被调用后:

我们观察,到底是哪里对[EBP-260]的值进行了修改:

[EBP-260] = 0x22FC18

 

 

一个Ring3程序对起始地址为0x2c的数据进行写入操作,显然是会导致崩溃的。

其根本原因是以__cdecl方式调用GetModuleFileNameExA函数后未恢复栈顶指针,导致后续函数在栈上的溢出,也就是修改了” ImgFileName”的起始地址,导致后续的对错误地址数据的写入操作,最终导致的程序运行崩溃。

 

7)        问题解决

再看MSDN,发现:

“WINAPI”在“windef.h”中的定义:

由此可知,导致程序崩溃的原因其实是调用方式的错误。将“typedef unsigned long (*MY_GET_MODULE_FILE_NAME_EX)(HANDLE hProcess, HMODULE hModule, char *lpFilename, unsigned long nSize);”改成“typedef unsigned long (__stdcall *MY_GET_MODULE_FILE_NAME_EX)(HANDLE hProcess, HMODULE hModule, char *lpFilename, unsigned long nSize);”后,问题解决。

 后记:当一个函数可能会被不同地方较频繁地调用的话(前提是不能inline化),用__stdcall比较好。因为用__cdecl的话,每个调用的地方都会产生一部分恢复栈的代码,这样生成的可执行文件会比__stdcall(恢复栈的代码在函数内部)方式调用的大。