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

经过几经排查发现是和调用规约(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. 使用DBCP连接池对连接进行管理

    //需要引用的jar包有4个,分别是commons-pool2-2.4.2.jar.commons-dbcp2-2.1.1.jar.mysql-connector-java-5.1.42-bin.ja ...

  2. jar包和war包

    Jar (Java archive), 是将实现了某功能的所有类及辅助资源用ZIP压缩形式打包而成的一个文件, 便于代码的管理和重复使用.当使用别人提供的jar时,只需要在classpath环境变量中 ...

  3. 为什么要使用addEventListener而不是on监听事件

    昨天回答了一个关于vue的问题 vue 除了input 其他可以用keyup事件嘛? 在vue中没有提供除表单之外其它的keyup绑定方法,可以使用原生的监控键盘的事件,于是给出了代码: mounte ...

  4. eclipse内存溢出设置

    1. arguments中的内容添加红色部分:   -Dcatalina.base="E:\workspace\.metadata\.plugins   \org.eclipse.wst.s ...

  5. 微信小程序实战(商城)

    github地址(欢迎star):https://github.com/xiaobinwu/dj 版本:0.15.152900(暂未升级原因:升级后需要图片无法本地引用,必须使用image或是远程路径 ...

  6. Easy machine learning pipelines with pipelearner: intro and call for contributors

    @drsimonj here to introduce pipelearner – a package I'm developing to make it easy to create machine ...

  7. 项目管理之 使用 appledoc 生成开发文档

    写项目时通常会遇到要求写开发文档的需求,但是就源代码来说,文档最好和源码在一起,这样更新起来更加方便和顺手.Objective-C 有一些文档管理工具,doxygen, headdoc 和 apple ...

  8. git使用3

    如何使用/学习第三方框架? 优秀的第三方框架都在 github.com 1> 搜索 2> git clone 获得完整版本 $ git clone https://github.com/A ...

  9. weex入门

    近期要做一个安卓端的原生应用程序.情况是这样的:需求方原先已经实现了网页,是一个工具类应用,大致作用是连接到他们公司生产的硬件,然后通手机与智能硬件通信来对硬件进行一系列控制.不过呢,这个网页先前是由 ...

  10. B树和B+树的总结

    B树 为什么要B树 磁盘中有两个机械运动的部分,分别是盘片旋转和磁臂移动.盘片旋转就是我们市面上所提到的多少转每分钟,而磁盘移动则是在盘片旋转到指定位置以后,移动磁臂后开始进行数据的读写.那么这就存在 ...