C/C++函数调用约定

函数调用约定就是函数调用者和被调用者之间的约定。函数调用约定决定了参数怎么传递以及由谁来平衡堆栈。

不同的编译器实现不一样。调用约定不显示指定的话默认是__cdecl。

这里主要讨论__cdecl和__stdcall

  • 共同点:__cdecl和__stdcall参数入栈顺序都是从右到左
  • 不同点:
  1. __cdecl由调用者清理堆栈,__stdcall由被调用者清理堆栈。
  2. 由于条件1的关系,约定是__cdecl的函数的参数可以是不定参数,约定是__stdcall的函数的参数不能是不定参数。

先看一个demo了解可变参数的实现:

#include <windows.h>
#include <tchar.h> 
#include <stdio.h>

int CDECL MessageBoxPrintf(TCHAR * szCaption, TCHAR * szFormat, ...)
{
    TCHAR szBuffer[1024];
    // typedef char* va_list;
    va_list pArgList;
    // The va_start macro (defined in STDARG.H) is usually equivalent to:
    // pArgList = (char *) &szFormat + sizeof (szFormat) ;

    wprintf(L"pArgList= %d", &szFormat);
    va_start(pArgList, szFormat);
    wprintf(L"pArgList= %d", pArgList);
    // The last argument to wvsprintf points to the arguments
    _vsntprintf(szBuffer, sizeof(szBuffer) / sizeof(TCHAR), szFormat, pArgList);
    // The va_end macro just zeroes out pArgList for no good reason
    va_end(pArgList);
    return MessageBox(NULL, szBuffer, szCaption, 0);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    int cxScreen, cyScreen;
    cxScreen = GetSystemMetrics(SM_CXSCREEN);
    cyScreen = GetSystemMetrics(SM_CYSCREEN);
    MessageBoxPrintf(TEXT("ScrnSize"),
        TEXT("The screen is %i pixels wide by %i pixels high."),
        cxScreen, cyScreen);
    return 0;

}

MessageBoxPrintf是__cdecl,参数是可变的。

如下图,在内存中,栈地址是从高地址向地地址扩展的,该demo中,TEXT2,TEXT1,szFormat,szCaption先后入栈。通过va_start(pArgList, szFormat);也就是szFormat+(szFormat)我们可以得到第一个不定参数也就是TEXT1(TEXT("ScrnSize")的地址,随后对这个地址偏移打印所有的不定参数。

为什么可变参数的函数只能用__cdecl?

这个目前还不明白。感觉是因为__cdecl是由调用者清理堆栈。猜测是__cdecl函数的调用者知道有多少个参数,所以可以正确清理堆栈。

接下来我们再来看看什么是被调用者清理堆栈,什么是调用者清理堆栈。

先看一个demo:

void __cdecl __cdecl_callee(int a, int b)
{
	cout << "__cdecl, a = " << a << ", b = " << b << endl;
}

void __stdcall __stdcall_callee(int a, int b)
{
	cout << "__stdcall, a = " << a << ", b = " << b << endl;
}

函数__cdecl_callee是__cdecl,函数__stdcall_callee是__stdcall,先来模拟它们的汇编实现:

void __cdecl_caller()
{
	int x = 1;
	int y = 2;
    __asm
    {
        push y
        push x
        call __cdecl_callee
    }
}

void __stdcall_caller()
{
	int x = 1;
	int y = 2;
	__asm
	{
		push y
		push x
		call __stdcall_callee
	}
}

分别运行代码发现,__stdcall_caller正常输出,__cdecl_caller则抛出如下异常:

原因是__stdcall_caller中堆栈由__stdcall_callee自己清理了。而__cdecl_caller中,调用者也就是__cdecl_caller本身没有释放堆栈,所以抛出该异常。

为了正确运行,我们对__cdecl_caller做出修改,pop x和y,程序正常输出:

void __cdecl_caller()
{
	int x = 1;
	int y = 2;
    __asm
    {
        push y
        push x
        call __cdecl_callee
	pop x
	pop y
    }
}

或者可以这么改,恢复esp寄存器的位置:

void __cdecl_caller_v2()
{
    unsigned long dwEsp;
	int x = 1;
	int y = 2;
    __asm
    {
        mov dwEsp, esp
        push y
        push x
        call __cdecl_callee
        mov esp, dwEsp
    }
}

这里以VS2015编译器为例,我们来看看__stdcall和__cdecl的汇编实现有什么区别:

如下图我在这四个地方分别打断点:

汇编代码如下图:

可以看到__cdecl在__cdecl_disassembly_caller中通过add esp,8 恢复堆栈。

而__stdcall则在__stdcall_disassembly_callee中通过ret 8 恢复堆栈。

个人认为恢复堆栈可以看作esp寄存器的位置恢复,add esq,8和ret 8都是将esq的地址加上8,即sizeof(a)+sizeof(b)。

到这里总算是对__cdecl和__stdcall有个基本的认识了。

在实际开发中,函数调用的时候要看清楚调用约定。比如函数caller的参数是个__stdcall的函数指针,如果我传个__cdecl的函数的地址给caller则会出现上面的异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值