最近在开发的过程中遇到了几个很诡异的问题,造成了栈不平衡从而导致程序崩溃。

经过几经排查发现是和调用规约(calling convention)相关的问题,特此分享出来。

首先,讲一下什么是调用规约

函数调用规约,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用规约就是描述参数是怎么传递由谁平衡堆栈的,当然还有返回值。

名称 谁负责参数出栈 参数压栈顺序
Cdecl Caller(调用者) 从右往左
Pascal Callee(被调用者) 从左往右
Stdcall  Callee(被调用者) 从右往左
Fastcall Callee(被调用者) 从右往左
Thiscall Callee(被调用者) 从右往左

下面,本文给出一个简短的代码示例:

 #include<iostream>

 typedef void(*funcPointer)(int);

 void __stdcall testFunc(int i, int j)
{
std::cout << "i is:" << i << std::endl;
std::cout << "j is:" << j << std::endl;
return ;
} void callFunc(void(*func)(int))
{
func();
} void main()
{
callFunc((funcPointer)testFunc);
getchar();
}

在第12行代码定义的callFunc函数,它的参数是一个“返回值为void,参数为一个int型的函数指针”,并在内部调用这个函数指针传实参为1。

在地5行代码定义了函数testFunc,它的参数为两个int,同时为它定义了__stdcall的调用约定。

在main函数的19行中进行调用的时候,对testFunc使用了(funcPointer)进行强行类型转换,并将它传入callFunc作为实参进行调用。

x86平台Debug版本运行这段程序的结果如下:

程序因为异常停在第19行了。

X86平台Release版本运行这段程序的结果如下:

程序虽然也执行到结尾了,但是由于传参不正确,所以结果不对,而且也无法正常停机。

X64平台Debug版本和Release版本运行这段程序的结果类似如下:

程序没有发生异常,顺利执行完毕,只是运行结果不正确。

为什么是这样一个结果呢?下面,本文就来细细讲解。

在计算机中有两个寄存器称为ebpesp,它们分别称为基址指针寄存器和堆栈指针寄存器。

esp和ebp分别指向当前运行函数的栈顶和栈底。

由于callFunc是以cdecl的方式进行声明的,而testFunc是以stdcall的方式进行声明的。因此在callFunc调用testFunc时,调用者(caller)负责将参数push进栈。因为此时testFunc已经进行了强行类型转换,因此编译器认为它的输入参数即为1个int,所以在入栈时callFunc将1个int压入堆栈中,接着调用testFunc。当testFunc执行完毕之后,由于它是stdcall所以由被调用者(callee)即testFunc自身负责参数的pop退栈。而此时,由于testFunc函数本身只有2个int型参数,所以在出栈时即pop两个int,导致了栈不平衡问题的产生!(而且在执行完testFunc之后,由于callFunc是cdecl类型的所以它仍然会再进行退栈的操作)如下图所示。

此处,截取了实际程序的反汇编代码进行分析:

上图是testFunc的反汇编,为了使反汇编看起来没那么冗长作者将其中一些代码注释掉了。可以看到,最后在结束时进行了ret 8的操作,即向上退8(两个int的大小)。(此处可以看到stdcall声明的函数进行自行参数退栈的实现)

上图是callFunc的反汇编,可以看到在调用子函数结束之后它进行了esp+4的操作,即退栈1个int(因为栈的地址空间是从大向小增长的所以是加操作)。

而且最后在它ret时是没有跟参数的,代表cdecl的函数不进行自我参数退栈操作。

关于Debug和Release,X86和X64结果不一样的原因

①在Debug版本下,Visual Studio的编译器会自动在编译参数中加入/RTC,即Runtime Check。启用运行时错误检查。其中包括了:堆栈指针验证,该操作检测堆栈指针损坏。 调用约定不匹配可能导致堆栈指针损坏。 例如,使用函数指针调用 DLL 中作为 __stdcall 导出的函数,但将指向该函数的指针声明为 __cdecl。此时编译器会在每个函数的开始和结束处加入针对esp指针的检查。详见:MSDN_CL_编译参数_RTC

因此在Debug版本下会报出上文所提到的异常。而在Release版本下,因为默认不进行太多检查即RTC被关闭,因此并不会出现弹出异常提示的情况。

Debug版反汇编代码如下:

可以从反汇编的代码中看到,在进入子函数之前先将esp的值保存在esi中,当执行完毕之后对比esi和现在的esp的值,即RTC。

②在X86版本下,在退栈时是以esp中的值为基址进行加减操作来进行的。而RTC又是对esp指针进行检查,因此此时会报出异常。

而在X64版本下,在退栈时是以ebp中的值为基址进行加减操作来进行的,RTC检查的是esp,毫不相关,所以不会抱任何异常。

诚然,这只是一个小“缺陷”,很多人认为不必在意。但是小小的问题也会在某一刻产生巨大的隐患,造成整个软件的崩溃。

从栈不平衡问题 理解 calling convention的更多相关文章

  1. X86调用约定 calling convention

    http://zh.wikipedia.org/wiki/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A 这里描述了在x86芯片架构上的调用约定(calling con ...

  2. Calling Convention的总结

    因为经常需要和不同的Calling Convention打交道,前段时间整理了一下它们之间的区别,如下: 清理堆栈 参数压栈顺序 命名规则 (MSVC++) 备注 Cdecl 调用者 (Caller) ...

  3. C&C++ Calling Convention

    tkorays(tkorays@hotmail.com) 调用约定(Calling Convention) 是计算机编程中一个比较底层的设计,它主要涉及: 函数参数通过寄存器传递还是栈? 函数参数从左 ...

  4. C/C++:函数的调用约定(Calling Convention)和名称修饰(Decorated Name)以及两者不匹配引起的问题

    转自:http://blog.csdn.net/zskof/article/details/3475182 注:C++有着与C不同的名称修饰,主要是为了解决重载(overload):调用约定则影响函数 ...

  5. Javascript的堆和栈的简单理解

    <!doctype html> <html> <head> <meta charset="UTF-8"> <title> ...

  6. function calling convention

    这是2013年写的一篇旧文,放在gegahost.net上面 http://raison.gegahost.net/?p=31 February 19, 2013 function calling c ...

  7. C#堆和栈的入门理解

    声明:以下内容从网络整理,非原创,适当待入个人理解. 解释1.栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义:堆是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定 ...

  8. 汇编中call printf参数压栈时错误理解

    EAX, ECX,EDX,EBX均可以32bit,16bit,8bit访问,如下所示: <-------------------EAX------------------------>|& ...

  9. iOS:堆(heap)和栈(stack)的理解

    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release 栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就 ...

随机推荐

  1. springcloud(五):熔断监控Hystrix Dashboard和Turbine

    Hystrix-dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard我们可以在直观地看到各Hystrix Command的请求响应时间, 请求成功率等数 ...

  2. sublime比较好用的插件

    emmet, markdown preview, package Control, SFTP, Anaconda

  3. Sampling Distributions and Central Limit Theorem in R(转)

    The Central Limit Theorem (CLT), and the concept of the sampling distribution, are critical for unde ...

  4. 解决相关css基础问题

    //html代码 <div class="operateWays"> <label> <input type="radio" na ...

  5. linux平台下Hadoop下载、安装、配置

    在这里我使用的linux版本是CentOS 6.4      CentOS-6.4-i386-bin-DVD1.iso      下载地址: http://mirrors.aliyun.com/cen ...

  6. Ubuntu安装genymotion模拟器步骤

    1.安装VitrualBox genymotion模拟器需要有VirtualBox环境,打开终端(ctrl + alt + T),执行以下命令: sudo apt-get install virtua ...

  7. javaScript 设计模式系列之二:适配器模式

    介绍 适配器模式将一个类的接口转接成用户所期待的,有助于避免大规模改写现有客户代码. In software engineering, the adapter pattern is a softwar ...

  8. (转)让浏览器支持Webp

    转载:https://segmentfault.com/a/1190000005898538?utm_source=tuicool&utm_medium=referral Webp介绍 web ...

  9. python爬虫之re正则表达式库

    python爬虫之re正则表达式库 正则表达式是用来简洁表达一组字符串的表达式. 编译:将符合正则表达式语法的字符串转换成正则表达式特征 操作符 说明 实例 . 表示任何单个字符 [ ] 字符集,对单 ...

  10. [原创] 利用前端+php批量生成html文件,传入新文本,输出新的html文件

    本人因为要想自己写个小说网站练练手,在其中遇到的一些问题,将其解决方法总结出来,例如: 1:小说网站存储了大量的小说,每个小说主页都很相似,url不同,不是使用的history属性改写的,所以如果人工 ...