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

经过几经排查发现是和调用规约(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. ng-class改变css样式

    而在这所谓的样子当然就是改变其css的属性,而实现能动态的改变其属性值,必然只能是更换其class属性 这里有三种方法: 第一种:通过数据的双向绑定(不推荐) 第二种:通过对象数组 第三种:通过key ...

  2. OpenCV探索之路(三):滤波操作

    滤波处理分为两大类:线性滤波和非线性滤波.OpenCV里有这些滤波的函数,使用起来非常方便,现在简单介绍其使用方法. 线性滤波:方框滤波.均值滤波.高斯滤波 方框滤波 #include<open ...

  3. 关于cgi、FastCGI、php-fpm、php-cgi

    搞了好长时间的php了,突然有种想法,想把这些整理在一起,于是查看各种资料,找到一片解释的很不错的文章,分享一下-- 首先,CGI是干嘛的?CGI是为了保证web server传递过来的数据是标准格式 ...

  4. 使用SpringBoot快速构建应用程序

    1.Spring MVC和Spring Boot自带的web构建方式有所区别.Spring提供了spring-boot-starter-web自动配置模块. 2. 添加如下依赖 <depende ...

  5. DropDownList如何绑定DataTable,如何绑定DataSet

    dpDnUpMenu是我定义的DropDownList控件 如果直接使用下面的方式,则会出现如下错误 dpDnUpMenu.DataSource = menu_tbBll.GetPID() dpDnU ...

  6. python 计算两个日期相差多少个月

    近期,由于业务需要计算两个日期之前相差多少个月.我在网上找了很久,结果发现万能的python,居然没有一个模块计算两个日期的月数,像Java.C#之类的高级语言,都会有(date1-date2).mo ...

  7. Java如何转换protobuf-net中的bcl.DateTime对象

    一.定义DateTime Message 参考文档:https://github.com/mgravell/protobuf-net/blob/master/src/Tools/bcl.proto m ...

  8. C语言之循环结构

    程序结构: 顺序结构 条件结构(分支结构) if结构,if-else结构 ,多重if分支结构,switch结构 循环结构:做重复的事情 while循环,do..while循环和for循环. 写循环结构 ...

  9. JavaSE教程-04Java中循环语句for,while,do···while-思维导图

    思维导图看不清楚时: 1)可以将图片另存为图片,保存在本地来查看 2)右击在新标签中打开放大查看

  10. cpp(第十章)

    1. const class & func(const class &) const { do something.. } 第一个const返回后的类不允许被赋值,第二个const不允 ...