首页 > 其他分享 >【文档翻译】__cdecl/__stdcall/__fastcall?解开神秘的调用约定!

【文档翻译】__cdecl/__stdcall/__fastcall?解开神秘的调用约定!

时间:2023-12-02 22:55:20浏览次数:39  
标签:__ 调用 函数 cdecl 约定 mov fastcall push

本文档译自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文参见此处


引言 - Introduction

在学习 Windows 编程的漫长、艰难而美妙的旅途中,你可能会对函数声明前出现的奇怪说明符感到好奇,比如 __cdecl__stdcall__fastcallWINAPI 等等。在阅读过 MSDN 或其他参考资料之后,你可能知道了这些说明符是用来为函数指定一种叫“调用约定”的东西。在这篇文章中,我会使用 Visual C++ 来向你解释不同的调用约定。我要强调的是,上面提到的说明符是微软特有的,如果你想编写可移植代码,就不应该使用它们。

那么,调用约定究竟是什么呢?当我们调用函数时,通常会将参数传递给它,并获得返回值。而调用约定就描述了参数是如何传递、值是如何从函数返回的。它还指定了函数名称的修饰方式。不过,编写优秀的 C/C++ 程序真的一定要了解调用约定吗?并不是。但是,它可能有助于调试。此外,如果要把 C/C++ 与汇编代码链接,那么这也有帮助。

要理解本文,你需要具备汇编编程的一些非常基本的知识。

无论使用哪种调用约定,都会发生以下情况:

  1. 所有参数都被扩展到 4 字节(除非特别说明,默认在 Win32 上),并放入内存的适当位置,这些位置通常在栈上。不过它们也可能被放在寄存器中,这便是通过调用约定指定的。
  2. 程序执行流会跳转到被调用函数的地址。
  3. 在函数内部,寄存器 ESI、EDI、EBX 和 EBP 的值被保存在栈上。执行这些操作的代码部分称为 function prolog,通常由编译器生成。
  4. 执行函数代码,并将返回值放入 EAX 寄存器中。
  5. 寄存器 ESI、EDI、EBX 和 EBP 的值从栈中恢复。执行此操作的代码段称为 function epilog,与 function prolog 一样,在大多数情况下,它由编译器生成。
  6. 参数从栈中移除。此操作称为清栈(stack cleanup),可以在被调用函数的内部执行,也可以由调用方执行,具体取决于所使用的调用约定。

作为调用约定的例子(不考虑 this),我们将使用一个简单的函数:

int sumExample (int a, int b)
{
    return a + b;
}

对这个函数的调用看起来像这样:

int c = sum (2, 3);

对于使用 __cdecl__stdcall__fastcall 的例子,我会把示例代码编译成 C 代码。本文后面提到的函数名修饰用的是 C 的修饰方法。C++ 的名称修饰方法超出了本文的讨论范围。


C 调用约定 - C calling convention (__cdecl)

这个约定是 C/C++ 的默认调用约定。如果项目被设置成使用其他的调用约定,我们也可以通过显式声明 __cdecl 来为某个函数指定:

int __cdecl sumExample (int a, int b);

__cdecl 调用约定的主要特点是:

  1. 参数将从右到左依次压入栈中。
  2. 由调用者执行清栈。
  3. 函数名用下划线字符 _ 作为前缀进行修饰。

现在,示例函数的调用看起来像这样:

; // 参数从右到左依次压入栈中
push        3    
push        2    

; // 调用函数
call        _sumExample 

; // 增加参数的总大小到 ESP 寄存器(向高位移动栈指针),以此来清理堆栈
add         esp,8 

; // 将 EAX 的返回值复制到局部变量 (int c)
mov         dword ptr [c],eax

被调用函数 sumExample 的内部如下所示:

; // function prolog
  push        ebp  
  mov         ebp,esp 
  sub         esp,0C0h 
  push        ebx  
  push        esi  
  push        edi  
  lea         edi,[ebp-0C0h] 
  mov         ecx,30h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  
; //    return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 

; // function epilog
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret



标准调用约定 - Standard calling convention (__stdcall)

这个调用约定常常用在 Win32 API 的函数上。事实上,WINAPI 只是 __stdcall 的另一个名称。

#define WINAPI __stdcall

同样,可以为一个函数显式指定标准调用约定:

int __stdcall sumExample (int a, int b);

我们也可以使用编译器选项 /Gz 来给所有未显式声明约定的函数指定 __stdcall

__stdcall 调用约定的主要特点是:

  1. 参数将从右到左依次压入栈中。
  2. 由被调用的函数执行清栈。
  3. 函数名通过添加下划线 _@ 字符和所需的堆栈空间字节数来修饰。

调用示例如下:

; // 参数从右到左依次压入栈中
  push        3    
  push        2    
  
; // 调用函数
  call        _sumExample@8

; // 将 EAX 的返回值复制到局部变量 (int c)
  mov         dword ptr [c],eax

函数如下所示:

; // 此处是 function prolog (和 __cdecl 的例子一样,略过)

; //    return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 

; // 此处是 function epilog (和 __cdecl 的例子一样,略过)

; // 清栈并返回控制流
  ret         8

因为栈由被调用的函数清理,所以通常 __stdcall 调用约定创建的可执行文件比 __cdecl 要小。因为在 __cdecl 中,必须为每个函数调用生成清栈的代码。另一方面,参数数量可变的函数(如 printf())必须使用 __cdecl,因为只有调用者知道函数调用中的参数数量;所以,也只有调用方才能执行清栈。


Fast 调用约定 - Fast calling convention (__fastcall)

__fastcall指出,只要有可能,参数就应该放在寄存器中,而不是栈中。这减少了函数调用的成本,因为使用寄存器的操作比使用堆栈的操作要快。

我们可以显式声明 __fastcall 来使用约定,如下所示:

int __fastcall sumExample (int a, int b);

我们也可以使用编译器选项 /Gr 来给所有未显式声明约定的函数指定 __fastcall

__fastcall 的主要特点是:

  1. 需要 32 位大小(及以下)的前两个函数参数被放入寄存器 ECX 和 EDX。其余的从右向左压入堆栈。
  2. 被调用的函数负责从堆栈中弹出参数。
  3. 函数名通过在开头添加 @ 字符并附加 @ 和参数所需的字节数(十进制)来修饰。

注意:Microsoft 保留在未来的编译器版本中更改传递参数的寄存器的权利。

调用例子如下:

; // 将参数放入寄存器 EDX 和 ECX 中
  mov         edx,3 
  mov         ecx,2 
  
; // 调用函数
  call        @fastcallSum@8
  
; // 从寄存器 EAX 拷贝返回值到局部变量 (int c)  
  mov         dword ptr [c],eax

函数内部:

; // function prolog
  push        ebp  
  mov         ebp,esp 
  sub         esp,0D8h 
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0D8h] 
  mov         ecx,36h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  pop         ecx  
  mov         dword ptr [ebp-14h],edx 
  mov         dword ptr [ebp-8],ecx 
; // return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 
;// function epilog  
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret

这个调用约定究竟和 __cdecl__stdcall 相比有多快呢?你可以自己寻找答案。通过声明不同的约定,再比较执行时间看看吧。我没有发现 __fastcall 比其他调用约定更快,不过你可能会得出不同的结论。


Thiscall - Thiscall

Thiscall 是调用 C++ 类成员函数的默认调用约定(参数数量可变的除外)。

这种约定的主要特点是:

  1. 参数将从右到左依次压入栈中。this被放在 ECX 寄存器中。
  2. 由被调用的函数执行清栈。

这个调用约定的例子有点不同。首先,代码被编译为 C++,而不是 C。其次,我们用一个带有成员函数的结构体,而不是用自由函数。

struct CSum
{
    int sum ( int a, int b) {return a+b;}
};

函数调用的汇编代码如下所示:

push        3
push        2
lea         ecx,[sumObj]
call        ?sum@CSum@@QAEHHH@Z            ; CSum::sum
mov         dword ptr [s4],eax

函数内部如下所示:

push        ebp
mov         ebp,esp
sub         esp,0CCh
push        ebx
push        esi
push        edi
push        ecx
lea         edi,[ebp-0CCh]
mov         ecx,33h
mov         eax,0CCCCCCCCh
rep stos    dword ptr [edi]
pop         ecx
mov         dword ptr [ebp-8],ecx
mov         eax,dword ptr [a]
add         eax,dword ptr [b]
pop         edi
pop         esi
pop         ebx
mov         esp,ebp
pop         ebp
ret         8

如果我们有一个成员函数使用可变数量参数会发生什么?在这种情况下,会使用 __cdeclthis 最后被压入栈。


总结 - Conclusion

长话短说,我们总结调用约定之间的主要区别:

  • __cdecl 是 C 和 C++ 程序的默认调用约定。这种调用约定的优点是,它允许使用具有可变数量参数的函数。缺点是它会创建更大的可执行文件。
  • __stdcall 多用于 Win32 API 函数。它不允许函数具有可变数量的参数。
  • __fastcall 尝试将参数放在寄存器中,而不是堆栈中,从而使函数调用更快。
  • Thscall 调用约定是不使用可变参数的 C++ 成员函数使用的默认调用约定。

在大多数情况下,这就是你需要了解的关于调用约定的全部内容。

标签:__,调用,函数,cdecl,约定,mov,fastcall,push
From: https://www.cnblogs.com/Code-For-What/p/17872401.html

相关文章

  • 聪明办法学python第四次打卡
    循环for循环的特点基于提供的范围,重复执行特定次数的操作for循环嵌套#下面的代码将输出二维坐标defprintCoordinates(xMax,yMax):forxinrange(1,xMax+1):foryinrange(1,yMax+1):print(f"({x},{y})",end="")print()printCoordinat......
  • 12.2日记
    Scala是一门以Java虚拟机(JVM)为运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言(静态语言需要提前编译的如:Java、C、C++等,动态语言如:JS)。1)Scala是一门多范式的编程语言,Scala支持面向对象和函数式编程。(多范式,就是多种编程方法的意思。有面向过程、面......
  • 【小黑菌的周记】23年9月10日-17日 幸福
    [23年9月10日-17日幸福]推荐BGM:《一直很安静》空荡的街景,想找个人放感情,做这种决定,是寂寞与我为邻。我们的爱情,像你路过的风景,一直在进行,脚步却从来不会为我而停。标题写的是“幸福”——这并不是用来形容我现在的状态上大学以前以为自己这辈子都不会打游戏上......
  • 【ASP.NET Core】MVC过滤器:常见用法
    前面老周给大伙伴们演示了过滤器的运行流程,大伙只需要知道下面知识点即可:1、过滤器分为授权过滤、资源访问过滤、操作方法(Action)过滤、结果过滤、异常过滤、终结点过滤。上一次咱们没有说异常过滤和终结点过滤,不过老周后面会说的。对这些过滤器,你有印象就行了。2、所有过滤器接......
  • 初中英语优秀范文100篇-015An Unusual Experience-一次不同寻常的经历
    PDF格式公众号回复关键字:SHCZFW015记忆树1ItwasFiriday.翻译那天是星期五简化记忆星期五句子结构在句子“ItwasFriday”中,有以下成分:“It”是主语,作为一个不定代词,用来指代或代表前文提到的特定时间或事件。这里指代的是具体的某个时间或事件。“was”是......
  • VMware安装教程
    一、安装VMware前准备需要先安装VMware安装包和centos的镜像VMware下载网址:https://customerconnect.vmware.com/cn/downloads/info/slug/desktop_end_user_computing/vmware_workstation_pro/16_0Centos镜像下载网址: Indexof/centos/7.9.2009/isos/x86_64/|清华大学......
  • hive启动出现Either your MetaData is incorrect, or you need to enable "datanucleu
    hive启动出现:Requiredtablemissing:"`VERSION`"inCatalog""Schema"".DataNucleusrequiresthistabletoperformitspersistenceoperations.EitheryourMetaDataisincorrect,oryouneedtoenable"datanucleus.schema......
  • Leetcode刷题day4-链表.交换.删除.相交.环
    24.两两交换链表中的节点24.两两交换链表中的节点-力扣(LeetCode)给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(只能进行节点交换)。示例1:输入:head=[1,2,3,4]输出:[2,1,4,3]示例2:输入:head=[]输出:[......
  • 序列化
    一,序列化主要分为通过函数进行序列化与反序列化达到传输数据的效果。根据情况可分为两种。第一种,python与不同语言间进行交流,比如,后端语言,javacc++c#等,因为需要使用都可以识别的数据类型进行传输,所有便诞生了Json模块。Json模块主要分为四个功能,dumps、dump、loads、......
  • RabbitMQ 消息转换器
     代码示例:1.引入依赖<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>2.在启动类中创建Beanpackagecom.itheima;importorg.springframework.amqp.rabbit.core.Rabbi......