request-free-img

64位程序下的调用约定:Windows x64统一规范彻底解析

你是不是也曾被32位程序下的 __stdcall、__fastcall、__thiscall、__cdecl 等复杂的调用约定搞得头晕眼花。

今天这个视频,这些我统统不讲。

因为在64位的程序下,其实这些都已成为过去式。

调用约定的核心区别

这些调用约定主要区别在于使用栈还是寄存器传递参数、以及栈清理责任归属等区别。

然而不同语言可能存在着不同的调用约定导致开发者痛苦不堪。

于是在64位程序下,WINDOWS和LINUX都统一了调用约定。

Windows x64下的参数传递规则

在WINDOWS下整数和指针参数的传递:

  • 第 1 个参数会放入 RCX
  • 第 2 个参数会放入 RDX
  • 第 3 个参数放入 R8
  • 第 4 个参数放入 R9

超过的参数才通过栈传递。

而对于浮点参数,前4个参数分别放入 XMM0 到 XMM3中,超过4个才通过栈传递。

代码验证示例

// __declspec(noinline) 防止内联优化,便于观察汇编代码
extern "C" __declspec(noinline) void __cdecl intFunc(
int a,
int b,
int c,
int d,
int e) {
std::cout << "intFunc: a = " << a
<< ", b = " << b
<< ", c = " << c
<< ", d = " << d << std::endl;
}

extern “C” __declspec(noinline) void floatFunc( float a, float b, float c, float d, float e) { std::cout << “floatFunc: a = ” << a << “, b = ” << b << “, c = ” << c << “, d = ” << d << std::endl; }

int main() { // 第1个整数参数(a)传递到 RCX, // 第2个整数参数(b)传递到 RDX, // 第3个整数参数(c)传递到 R8, // 第4个整数参数(d)传递到 R9。 // 第5个整数参数 (e) 通过栈传递 intFunc(10, 20, 30, 40, 50);

// 第1个浮点参数(a)传递到 XMM0, // 第2个浮点参数(b)传递到 XMM1, // 第3个浮点参数(c)传递到 XMM2, // 第4个浮点参数(d)传递到 XMM3。 // 第5个浮点参数 (e) 通过栈传递 floatFunc(1.5f, 2.5f, 3.5f, 4.5f, 5.5f); return 1; }

在我们的示例代码中,intFunc函数有5个int参数用来演示整型参数的传递规则。

顺便说一下在这个函数中declaration specification noinline的作用是为了便于观察汇编代码防止内联优化。

另外我们还有一个floatFunc它有5个浮点参数。

然后我们在main函数中分别调用这2个函数,现在我在这里设置一个断点并运行这个程序。

整数参数传递汇编分析

intFunc(10, 20, 30, 40, 50);
00007FF63057259B mov dword ptr [rsp+20h],32h
00007FF6305725A3 mov r9d,28h
00007FF6305725A9 mov r8d,1Eh
00007FF6305725AF mov edx,14h
00007FF6305725B4 mov ecx,0Ah
00007FF6305725B9 call intFloatFunc (07FF6305712E4h)

因为函数参数是从右往左处理的,所以最下边这个参数是函数的第一个参数。

我们从下往上看:

  • 第一个参数被加载到rcx寄存器
  • 第二个加载到edx
  • 第三个加载到r8d
  • 第四个加载到r9d
  • 第五个参数就是通过栈传递了

RDX是64位的寄存器,而EDX只是RDX的低32位,因为我们传递的只是一个32位整数,所以只会用到低32位。

r8d表示只用到r8的低32位部分。

浮点参数传递汇编分析

floatFunc(1.5f, 2.5f, 3.5f, 4.5f, 5.5f);
00007FF6305725BE movss xmm0,dword ptr [__real@40900000 (07FF63057AC74h)]
00007FF6305725C6 movss dword ptr [rsp+20h],xmm0
00007FF6305725CC movss xmm3,dword ptr [__real@40600000 (07FF63057AC70h)]
00007FF6305725D4 movss xmm2,dword ptr [__real@40200000 (07FF63057AC6Ch)]
00007FF6305725DC movss xmm1,dword ptr [__real@3fc00000 (07FF63057AC68h)]
00007FF6305725E4 movss xmm0,dword ptr [string ", " (07FF63057AC64h)]
00007FF6305725EC call floatFunc (07FF63057122Bh)

我们依然是从下往上来看,第一个参数加载到XMM0中,然后依次把参数加载到xmm1,xmm2,xmm3中,超过4个参数的则使用栈传递。

特别注意的是:SSE指令不允许内存到内存的直接传输,必须要通过XMM寄存器中转。

不同调用约定在64位下的表现

有网友可能又要说了,你这个函数也没有加调用约定修饰呀。

行,我们现在就分别给这些函数加上调用约定。

__stdcall

加上 __stdcall 后,汇编代码没有任何变化,和之前我们看到的完全一样,还是分别采用rcx, rdx, R8, R9传递的。

__fastcall

加上 __fastcall 调用约定后,汇编代码没有任何变化,和之前我们看到的也是完全一样。

__thiscall

加上 __thiscall 调用约定后,汇编代码没有任何变化,和之前我们看到的也是完全一样。

__cdecl

加上 __cdecl 调用约定后,汇编代码没有任何变化,和之前我们看到的也是完全一样。

重要结论

由此我们可以得到结论,在64位程序下不再区分繁杂的调用约定,而是采用了统一的X64调用约定。

这无疑大大降低了程序员的开发复杂度。

这里要特别注意的是,如果是类对象,类的this指针永远是做为第一个参数被传递的,也就是会放在RCX寄存器中。

混合整型与浮点参数的传递规则

extern "C" __declspec(noinline) void intFloatFunc(
int a,
float b,
int c,
float d
) {
std::cout << "intFunc: a = " << a
<< ", b = " << b
<< ", c = " << c
<< ", d = " << d << std::endl;
}

我们这个演示函数第一个参数是int,第二个是float型,第三个又是Int, 第4个又是float。

intFloatFunc(1, 1.5f, 2, 2.5f);
00007FF6872E604C movss xmm3,dword ptr [__real@40200000 (07FF6872EAC6Ch)]
00007FF6872E6054 mov r8d,2
00007FF6872E605A movss xmm1,dword ptr [__real@3fc00000 (07FF6872EAC68h)]
00007FF6872E6062 mov ecx,1
00007FF6872E6067 call intFloatFunc (07FF6872E1460h)

第一个参数是整型,所以被传入到rcx中。

第二个参数是浮点型,它被传入到xmm1中(跳过了xmm0)。

第三个参数是整型,它用的是R8寄存器(跳过了RDX)。

也就是说,针对整型参数,RCX是第1个槽位,RDX是第2个槽位;针对浮点数,XMM0是第1个槽位,XMM1是第2个槽位。

它将固定槽位的寄存器做了强制位置约定,然后按照参数的位置来使用对应寄存器槽位的。

总结

现在,关于64位程序下的调用约定你明白了吗?


更多问题探讨,请关注公众号:程序员角