__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位。