C/C |图文深入理解函数调用的5种约定

C/C |图文深入理解函数调用的5种约定

首页休闲益智堆栈平衡更新时间:2024-04-26

函数调用约定(Calling Convention),是一个重要的基础概念,用来规定调用者和被调用者是如何传递参数的,既调用者如何将参数按照什么样的规范传递给被调用者。

在参数传递中,有两个很重要的问题必须得到明确说明:

I 当参数个数多于一个时,按照什么顺序把参数压入堆栈;

II 函数调用后,由谁来把堆栈恢复原状。

假如在C语言中,定义下面这样一个函数:

int func(int x,int y, int z)

然后传递实参给函数func()就可以使用了。但是,在系统中,函数调用中参数的传递却是一门学问。因为在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机用栈来支持参数传递。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原状。

在高级语言中,通过函数调用约定来说明参数的入栈和堆栈的恢复问题。常见的调用约定有:

__stdcall __cdecl __fastcall thiscall __naked call

不同的调用规约,在参数的入栈顺序,堆栈的恢复,函数名字的命名上就会不同。在编译后的代码量,程序执行效率上也会受到影响。

在以上几种调用约定中,只有__cdecl是可以支持变长参数的(如支持printf()参数数量不确定的函数),由调用者负责堆栈管理,这两者是相关的,因为调用者负责传参,知道参数的数量和类型,如果要支持数量不确定的参数的话,只能是调用者来管理堆栈平衡。__cdecl也是C语言默认的调用约定。__stdcall每次函数调用都要由编译器产生还原堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多。

对于__cdecl,在call之前,会将参数压入堆栈,压入了参数占用多少个字节(需要考虑堆栈对齐的填充,假设是n),需要在ret后,将esp偏移n个字节。而__stdcall不同,直接ret偏移n个字节。(__stdcall也是在call之前将参数压入栈帧。)

示例代码:

#include <stdio.h> int __stdcall stdcallF(int x, int y); // 被调用函数自身修改堆栈,WINAPI和CALLBACK //int cdeclF(int x ,int y); // 默认的C调用约定 int __cdecl cdeclF(int x,int y); // 明确指出C调用约定, 调用者修改堆栈,支持可变参数,比如printf() int __fastcall fastcallF(int x,int y,int z); // 被调用者修改堆栈 int __declspec(naked) sub(int a,int b) // 编译器不会给这种函数增加初始化和清理代码, { // 也不能用return语句返回值,只能程序员控制, // 插入汇编返回结果。因此它一般用于驱动程序设计 __asm mov eax,a __asm sub eax,b __asm ret } class CAdd { int a,b; public: CAdd(int aa,int bb):a(aa),b(bb){} int add(int c); // 参数从右向左入栈,this指针最后入栈 // 参数个数不确定时,调用者清理堆栈,否则函数自己清理堆栈 }; int main() { int a = 3; int b = 4; int c = 5; int s = 0; sub(3,4); s = stdcallF(a,b); s = cdeclF(a,b); s = fastcallF(a,b,c); CAdd cadd(3,4); s = cadd.add(c); printf("%d\n",s); //38 getchar(); return 0; } int __stdcall stdcallF(int x, int y) // WINAPI和CALLBACK { // 被调用函数自身修改堆栈 return x y; } //int cdeclF(int x ,int y) // 默认的C调用约定 int __cdecl cdeclF(int x,int y) // 明确指出C调用约定 { // 调用者修改堆栈,支持可变参数,比如printf() return x y; } int __fastcall fastcallF(int x,int y,int z) // 被调用者修改堆栈 { // 函数的第一个和第二个参数通过ecx和edx传递,剩余参数从右到左入栈 // 在X64平台,默认使用了fastcall调用约定,因其有较多的寄存器 return x y z; } int CAdd::add(int c) // 参数从右向左入栈,this指针最后入栈 { // 参数个数不确定时,调用者清理堆栈,否则函数自己清理堆栈 return a b c; }

我们来看一下__cdecl调用的调用者的汇编:

34: s = cdeclF(a,b); 004010B0 mov edx,dword ptr [ebp-8] 004010B3 push edx // 压参b 004010B4 mov eax,dword ptr [ebp-4] 004010B7 push eax // 压参a 004010B8 call @ILT 10(cdeclF) (0040100f) 004010BD add esp,8 // 主调函数做堆栈平衡,2个int参数,esp偏移8字节 004010C0 mov ecx,dword ptr [ebp-10h] 004010C3 add ecx,eax 004010C5 mov dword ptr [ebp-10h],ecx // 返回值

__cdecl调用的被函数的汇编:

49: int __cdecl cdeclF(int x,int y) // 明确指出C调用约定 50: { // 调用者修改堆栈,支持可变参数,比如printf() 00401230 push ebp 00401231 mov ebp,esp 00401233 sub esp,40h 00401236 push ebx 00401237 push esi 00401238 push edi 00401239 lea edi,[ebp-40h] 0040123C mov ecx,10h 00401241 mov eax,0CCCCCCCCh 00401246 rep stos dword ptr [edi] 51: return x y; 00401248 mov eax,dword ptr [ebp 8] 0040124B add eax,dword ptr [ebp 0Ch] 52: } 0040124E pop edi 0040124F pop esi 00401250 pop ebx 00401251 mov esp,ebp 00401253 pop ebp 00401254 ret // 这里被调函数没有做堆栈平衡

__cdecl代表性的栈示意图:

__cdecl代表性的函数栈帧的示例代码:

#include <stdio.h> struct ST{ int a; double d; //ST(int aa,double dd):a(aa),d(dd){}; }; ST test(double d,int a) { char ch = 'a'; char chs[5] = ""; ST st; st.a = a; st.d = d; return st; } int main() { ST s = test(2.3,4); printf("%d %f\n",s.a,s.d); getchar(); return 0; }

ref:

http://mallocfree.com/basic/c/c-6-function.htm#79

-End-

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved