zoukankan      html  css  js  c++  java
  • __stdcall与__cdecl之区别浅析及相关知识

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

    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(恢复栈的代码在函数内部)方式调用的大。

    本文首发于博客园,任何其他站点均为爬虫或转载,爬虫最无耻。
  • 相关阅读:
    [No0000161]IDEA初步接触
    [No0000171]wpf 类层次结构Class Hierarchy
    [No0000160]常用C# 正则表达式大全
    [No000015D]【李笑来 笔记整理】个人商业模式升级
    thinkphp 系统变量
    thinkphp不读取.env文件的键对值
    thinkphp 模板变量输出替换和赋值
    thinkphp 视图view
    thinkphp 响应对象response
    Thinkphp 请求和响应
  • 原文地址:https://www.cnblogs.com/codeape/p/2740057.html
Copyright © 2011-2022 走看看