C++重载底层原理

C++重载原理

C++实现重载使用的是叫name mangling的技术,实际上是靠函数名称和传递的参数类型、数量生成一个新的函数名称,比如

void func(int);
void func(double);

(g++ xx.cpp -S后生成xx.s汇编文件)经过编译后的汇编符号变成

_Z4funci;
_Z4funcd;

通过这样的方法实现重载。

而C不能实现重载的原因是因为C不能通过函数名称和传递的参数来生成新的函数符号,所以c文件汇编后的函数名与源文件的函数基本一致。

tips:使用C++filt可以还原C++生成的函数符号

>C++filt _Z4funci
>fun(int)

再谈论一下函数调用约定

函数调用约定指的是函数参数的入栈顺序、位置以及由谁恢复堆栈的约定。

__cdecl

参数传递顺序由右到左,堆栈由调用方清理

int __cdecl func(int a1,int a2)
{
	return a1+a2;
}
int main()
{
    func(1,2);
    return 0;
}

汇编代码如下:

     5:     return a1 + a2;
00561831  mov         eax,dword ptr [a1]  
00561834  add         eax,dword ptr [a2]  
     6: }
00561837  pop         edi  
00561838  pop         esi  
00561839  pop         ebx  
0056183A  add         esp,0C0h  
00561840  cmp         ebp,esp  
00561842  call        __RTC_CheckEsp (056123Fh)  
00561847  mov         esp,ebp  
00561849  pop         ebp  
0056184A  ret
...
9:     func(1, 2);
005618D1  push        2  
005618D3  push        1  
005618D5  call        func (05613B6h)  
005618DA  add         esp,8

可以看出先将2压入栈,再压入1,最后由main函数恢复栈。

__stdcall

__stdcall 调用约定用于调用 Win32 API 函数。 参数从右到左进行入栈,被调用方将清理堆栈,以便让编译器生成 vararg 函数 __cdecl。 使用此调用约定的函数需要一个函数原型。 __stdcall 修饰符是 Microsoft 专用的。

vararg函数指接收可变数量参数的函数。

int __stdcall func(int a1,int a2)
{
	return a1+a2;
}
int main()
{
    func(1,2);
    return 0;
}

汇编代码如下:

5:     return a1 + a2;
00181836  or          al,5Fh  
6: }
00181838  pop         esi  
00181839  pop         ebx  
0018183A  add         esp,0C0h  
00181840  cmp         ebp,esp  
00181842  call        __RTC_CheckEsp (018123Fh)  
00181847  mov         esp,ebp  
00181849  pop         ebp  
0018184A  ret         8
    ...
9:     func(1, 2);
001818D1  push        2  
001818D3  push        1  
001818D5  call        func (01813BBh)

__fastcall

这个约定仅适用于x86结构,尽可能多得使用寄存器传递函数自变量,前两个参数从左往右存入寄存器ECX、EDX,多余的参数从右往左压入栈内,如果有多余的参数在栈中则由被调用者恢复。

int __fastcall func(int a1,int a2)
{
	return a1+a2;
}
int main()
{
    func(1,2);
    return 0;
}

汇编代码如下:

5: 	return a1 + a2;
00B9183D  mov         eax,dword ptr [a1] 
00B91854  in          eax,5Dh  
00B91856  ret         4  
...
9: 	func(1, 2, 3);
00B918D1  push        3  
00B918D3  mov         edx,2  
00B918D8  mov         ecx,1  
00B918DD  call        func (0B913C0h)

可以看出是先压多余的参数入栈,再将前两个参数存入寄存器中,我也尝试了一下只传一个参数是存ECX还是EDX,结果是ECX。


__thiscall

特定于 Microsoft __thiscall 的调用约定用于 x86 体系结构上的 C++ 类成员函数。 它是成员函数使用的默认调用约定,该约定不使用变量参数(vararg 函数)。

__thiscall 下,被调用方清理堆栈,这对于 vararg 函数是不可能的。 自变量将从右到左推送到堆栈中。 指针 this 通过注册 ECX 传递,而不是在堆栈上传递。

__thiscall是C++类成员函数所独有的,在汇编代码中通常是将this作为第一个参数传递。

//TODO:能力尚浅,后续再来研究

__vectorcall

要看懂这个约定首先得知道x86与x64的区别。

//TODO:x86与x64的区别

__clrcall

//TODO:托管函数,也是不太懂

// clrcall3.cpp
// compile with: /clr
void Test() {
   System::Console::WriteLine("in Test");
}

int main() {
   void (*pTest)() = &Test;
   (*pTest)();

   void (__clrcall *pTest2)() = &Test;
   (*pTest2)();
}

__nakedcall

一种比较少见的约定,常用于驱动开发。

参考文献

调用约定 | Microsoft Learn

posted @ 2024-09-20 16:43  Isakura红  阅读(52)  评论(0)    收藏  举报