C语言函数调用约定

C/C++
385
0
0
2024-02-17
__attribute__((cdecl)) int a1(int a,int b,int c,int d){
    return a + b + c + d;
}

__attribute__((fastcall)) int a2(int a,int b,int c,int d){
    return a + b + 2*c + d;
}

__attribute__((stdcall)) int a3(int a,int b,int c,int d){
    return 3*a + 2*b + 2*c + d;
} 

int main(){
    int a,b,c,d;
    a = 10;
    b = 20;
    c = 30;
    d = 40;
    a1(a,b,c,d);
    a2(a,b,c,d);
    a3(a,b,c,d);
}

gcc -m32 -g -o0 func.c -o funcdemo

-m32 强制编译为32位,-g带debug信息,-o0 编译器不进行优化, -o输出文件名

在Window下和wsl下编译都报错了,Ubuntu下成功编译,环境问题头痛啊!

objdump -S -M intel funcdemo

000011ad <a1>:
// #include  <stdio.h> 
__attribute__((cdecl)) int a1(int a,int b,int c,int d){
    11ad:       f3 0f 1e fb             endbr32 
    11b1:       55                      push   ebp   //保存ebp寄存器的栈顶指针,
                                                    //可以在函数退出时恢复,该寄存器将用来保存堆栈
    11b2:       89 e5                   mov    ebp,esp     //保存堆栈指针
    11b4:       e8 eb 00 00 00          call   12a4 <__x86.get_pc_thunk.ax>
    11b9:       05 23 2e 00 00          add    eax,0x2e23
    return a + b + c + d;
    11be:       8b 55 08                mov    edx,DWORD PTR [ebp+0x8]
    11c1:       8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]
    11c4:       01 c2                   add    edx,eax
    11c6:       8b 45 10                mov    eax,DWORD PTR [ebp+0x10]
    11c9:       01 c2                   add    edx,eax
    11cb:       8b 45 14                mov    eax,DWORD PTR [ebp+0x14]
    11ce:       01 d0                   add    eax,edx
}
    11d0:       5d                      pop    ebp
    11d1:       c3                      ret        //被调用函数直接rentun ,注意,这里没有修改堆栈
000011d2 <a2>:

__attribute__((fastcall)) int a2(int a,int b,int c,int d){
    11d2:       f3 0f 1e fb             endbr32 
    11d6:       55                      push   ebp
    11d7:       89 e5                   mov    ebp,esp
    11d9:       83 ec 08                sub    esp,0x8
    11dc:       e8 c3 00 00 00          call   12a4 <__x86.get_pc_thunk.ax>
    11e1:       05 fb 2d 00 00          add    eax,0x2dfb
    11e6:       89 4d fc                mov    DWORD PTR [ebp-0x4],ecx
    11e9:       89 55 f8                mov    DWORD PTR [ebp-0x8],edx
    return a + b + 2*c + d;
    11ec:       8b 55 fc                mov    edx,DWORD PTR [ebp-0x4]
    11ef:       8b 45 f8                mov    eax,DWORD PTR [ebp-0x8]
    11f2:       01 c2                   add    edx,eax
    11f4:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
    11f7:       01 c0                   add    eax,eax
    11f9:       01 c2                   add    edx,eax
    11fb:       8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]
    11fe:       01 d0                   add    eax,edx
}
    1200:       c9                      leave  
    1201:       c2 08 00                ret    0x8         // 表示清理8个字节的堆栈,2个参数在栈上
                                                           //函数自己恢复了堆栈
00001204 <a3>:

__attribute__((stdcall)) int a3(int a,int b,int c,int d){
    1204:       f3 0f 1e fb             endbr32 
    1208:       55                      push   ebp
    1209:       89 e5                   mov    ebp,esp
    120b:       e8 94 00 00 00          call   12a4 <__x86.get_pc_thunk.ax>
    1210:       05 cc 2d 00 00          add    eax,0x2dcc
    return 3*a + 2*b + 2*c + d;
    1215:       8b 55 08                mov    edx,DWORD PTR [ebp+0x8]     //a -> edx
    1218:       89 d0                   mov    eax,edx                     // edx -> eax   
    121a:       01 c0                   add    eax,eax                     // a + a -> eax   
    121c:       01 c2                   add    edx,eax                     // a+a+a -> edx
    121e:       8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]     //b -> eax       
    1221:       01 c0                   add    eax,eax                    // b+b -> eax
    1223:       01 c2                   add    edx,eax                     //3*a + 2*b ->edx   
    1225:       8b 45 10                mov    eax,DWORD PTR [ebp+0x10]       //c ->eax 
    1228:       01 c0                   add    eax,eax                        // c+c ->eax
    122a:       01 c2                   add    edx,eax                        //3*a+2*b+2*c->edx  
    122c:       8b 45 14                mov    eax,DWORD PTR [ebp+0x14]
    122f:       01 d0                   add    eax,edx
} 
    1231:       5d                      pop    ebp
    1232:       c2 10 00                ret    0x10       // 表示清理16个字节的堆栈,4个参数
                                                           //函数自己恢复了堆栈
00001235 <main>:

int main(){
    1235:       f3 0f 1e fb             endbr32 
    1239:       55                      push   ebp
    123a:       89 e5                   mov    ebp,esp
    123c:       83 ec 10                sub    esp,0x10
    123f:       e8 60 00 00 00          call   12a4 <__x86.get_pc_thunk.ax>
    1244:       05 98 2d 00 00          add    eax,0x2d98
    int a,b,c,d;
    a = 10;
    1249:       c7 45 f0 0a 00 00 00    mov    DWORD PTR [ebp-0x10],0xa      //a
    b = 20;
    1250:       c7 45 f4 14 00 00 00    mov    DWORD PTR [ebp-0xc],0x14       //b 
    c = 30;
    1257:       c7 45 f8 1e 00 00 00    mov    DWORD PTR [ebp-0x8],0x1e        //c
    d = 40;
    125e:       c7 45 fc 28 00 00 00    mov    DWORD PTR [ebp-0x4],0x28      //d
    a1(a,b,c,d);
    // cdecl ,C语言默认调用约定,参数通过从右向左的顺序压栈,调用者函数恢复堆栈
    1265:       ff 75 fc                push   DWORD PTR [ebp-0x4]            //d
    1268:       ff 75 f8                push   DWORD PTR [ebp-0x8]             //c   
    126b:       ff 75 f4                push   DWORD PTR [ebp-0xc]            //b
    126e:       ff 75 f0                push   DWORD PTR [ebp-0x10]            //a
    1271:       e8 37 ff ff ff          call   11ad <a1>                     //调用a1
    1276:       83 c4 10                add    esp,0x10            //注意:这里调用者在函数恢复堆栈
    a2(a,b,c,d);
    // fastcall ,函数的第一个和第二个DWORD参数通过ecx和edx传递(a->ecx,b->edx),
    //其他参数通过从右向左的顺序压栈,被调用函数清理堆栈
    1279:       8b 55 f4                mov    edx,DWORD PTR [ebp-0xc]         //b
    127c:       8b 45 f0                mov    eax,DWORD PTR [ebp-0x10]        //a
    127f:       ff 75 fc                push   DWORD PTR [ebp-0x4]            //d
    1282:       ff 75 f8                push   DWORD PTR [ebp-0x8]            //c
    1285:       89 c1                   mov    ecx,eax                        //a
    1287:       e8 46 ff ff ff          call   11d2 <a2>    // 调用后没有恢复堆栈操作,被调用函数恢复
    a3(a,b,c,d);
    //stdcall ,参数从右向左的顺序压栈,调用者函数清理堆栈
    128c:       ff 75 fc                push   DWORD PTR [ebp-0x4]            //d
    128f:       ff 75 f8                push   DWORD PTR [ebp-0x8]            //c
    1292:       ff 75 f4                push   DWORD PTR [ebp-0xc]            //b
    1295:       ff 75 f0                push   DWORD PTR [ebp-0x10]           //a
    1298:       e8 67 ff ff ff          call   1204 <a3>
    129d:       b8 00 00 00 00          mov    eax,0x0
}

一个程序由若干个函数组成,程序的执行实际上就是函数之间的相互调用。

函数调用方和被调用方必须遵守同样的约定,即调用约定(Calling Convention)。

一个调用惯例一般规定以下两方面的内容:

[函数参数的传递方式]:是通过栈传递还是通过寄存器传递;

[函数参数的传递顺序]:当参数个数多于一个时,按照什么顺序把参数压入栈?

是从左到右入栈还是从右到左入栈;

[参数弹出方式]:函数调用后,由谁来把栈恢复原状?

函数调用结束后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由调用方来完成,也可以由被调用方来完成。

[函数名修饰方式]:函数名在编译时会被修改,调用惯例可以决定如何修改函数名。

函数调用惯例在函数声明和函数定义时都可以指定,语法格式为:

‌返回值类型 调用惯例 函数名(函数参数)

int __cdecl max(int m, int n); // __cdecl是C语言默认的调用约定,在平时编程中,我们并没有去指定调用约定,就使用默认的 __cdecl。

__cdecl 并不是标准关键字,是在 VC/VS 下有效,但在 GCC 下,要使用 __attribute__((cdecl))。 __attribute__ 是属性声明,告诉编译器此变量/函数需要检查或优化。

除了 cdecl,还有其他调用约定:

调用约定 参数传递方式 参数出栈方式 名字修饰(编译器重命名函数)

cdecl 从右到左的顺序入栈 调用方(caller) _+function

stdcall 从右到左的顺序入栈 被调用方(callee) _+function+@+参数的字节数

fastcall 部分参数放入寄存器,剩下的参数按照从右到左的顺序入栈 被调用方(callee) @+function+@+参数的字节数

pascal 从左到右的顺序入栈 被调用方(callee) \

调用约定

参数传递方式

参数出栈方式

名字修饰(编译器重命名函数)

cdecl

从右到左的顺序入栈

调用方(caller)

_+function

stdcall

被调用方(callee)

_+function+@+参数的字节数

fastcall

函数的第一个和第二个DWORD参数通过ecx和edx传递,剩下的参数按照从右到左的顺序入栈

cdecl: C语言默认,变参函数

由于每次函数调用都要由编译器产生还原栈的代码,所以使用 __cdecl 方式编译的程序比使用 __stdcall 方式编译的程序要大很多。

但是 __cdecl 调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如 printf()和 Windows 的 API wsprintf()就是 __cdecl调用方式。

stdcall:Windows API、内核驱动

fastcall:x64

以 fastcall 声明执行的函数,具有较快的执行速度,因为前俩个参数通过寄存器来进行传递的。

x64平台,还有一些扩展…

一个函数在调用时,前四个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从右至左顺序入栈;栈的增长方向为从高地址到低地址。

浮点前4个参数传入XMM0、XMM1、XMM2 和 XMM3 中,其他参数传递到堆栈中。

调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;

调用者负责栈平衡;

被调用函数的返回值是整数时,则返回值会被存放于RAX;浮点数返回在xmm0中

RAX,RCX,RDX,R8,R9,R10,R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax, ecx, edx是易挥发的)

栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub rsp,28h

对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。