最近在学习csapp,遇到了这个东西。之前只是知道有这个东西,今天在这里总结一下。
x86-64调用规约
x64在不同编译器上都使用了fast_call。即函数调用者(caller) 负责清理堆栈。16字节栈对齐。使用一定数量的寄存器(不同的编译器还不一样。。。我这个还查了一下 关于为啥巨硬要自己搞一个还请看大佬讨论 )进行传递参数从左到右传递参数。超过寄存器的个数再通过栈从右到左传递。函数返回值放在eax里
g++/ clang 使用6个寄存器进行参数传递,分别是:edi, esi, edx, ecx, r8d和r9d。
msvc使用 4个寄存器进行参数传递,分别是:rcx, rdx, r8d, r9d。(其实这些是最简单的情况,如果出现浮点数还会使用其他寄存器,详情还是要看msdn msvc调用规约细节)。
下面来看一个实验代码。
#include<iostream>
int func_5_arg(int a, int b, int c, int d, int e){
return a + b + c + d + e;
}
int func_6_arg(int a, int b, int c, int d, int e, int f){
return a + b + c + d + e + f;
}
int func_7_arg(int a, int b, int c, int d, int e, int f, int g){
return a + b + c + d + e + f + g;
}
int func_8_arg(int a, int b, int c, int d, int e, int f, int g, int h){
return a + b + c + d + e + f + g + h;
}
int main(){
int a,b,c,d,e,f,g,h;
a = 1;
b = 2;
c = 3;
d = 4;
e = 5;
f = 6;
g = 7;
h = 8;
int res5 = func_5_arg(a, b, c, d, e);
std::cout << res5;
int res6 = func_6_arg(a, b, c, d, e, f);
std::cout << res6;
int res7 = func_7_arg(a, b, c, d, e, f, g);
std::cout << res7;
int res8 = func_8_arg(a, b, c, d, e, f, g, h);
std::cout << res8;
return 0;
}
实验代码也很简单,就是使用不同个数的函数参数来看一下编译器生成的机器代码。这里用objdump来反汇编机器代码,输出如下,由于我习惯看intel的就没使用默认的att风格的汇编。这里使用的是g++,编译参数为 g++ -O1
先看5个参数和6个参数的
0000000100003e40 __Z10func_5_argiiiii:
100003e40: 55 push rbp
100003e41: 48 89 e5 mov rbp, rsp
100003e44: 8d 04 37 lea eax, [rdi + rsi]
100003e47: 01 d0 add eax, edx
100003e49: 01 c8 add eax, ecx
100003e4b: 44 01 c0 add eax, r8d
100003e4e: 5d pop rbp
100003e4f: c3 ret
0000000100003e50 __Z10func_6_argiiiiii:
100003e50: 55 push rbp
100003e51: 48 89 e5 mov rbp, rsp
100003e54: 8d 04 37 lea eax, [rdi + rsi]
100003e57: 01 d0 add eax, edx
100003e59: 01 c8 add eax, ecx
100003e5b: 44 01 c0 add eax, r8d
100003e5e: 44 01 c8 add eax, r9d
100003e61: 5d pop rbp
100003e62: c3 ret
100003e63: 66 2e 0f 1f 84 00 00 00 00 00 nop word ptr cs:[rax + rax]
100003e6d: 0f 1f 00 nop dword ptr [rax]
对应的main函数的部分
100003eb6: bf 01 00 00 00 mov edi, 1
100003ebb: be 02 00 00 00 mov esi, 2
100003ec0: ba 03 00 00 00 mov edx, 3
100003ec5: b9 04 00 00 00 mov ecx, 4
100003eca: 41 b8 05 00 00 00 mov r8d, 5
100003ed0: e8 6b ff ff ff call -149 <__Z10func_5_argiiiii>
100003ed5: 48 8b 1d 24 01 00 00 mov rbx, qword ptr [rip + 292]
100003edc: 48 89 df mov rdi, rbx
100003edf: 89 c6 mov esi, eax
100003ee1: e8 a8 00 00 00 call 168 <dyld_stub_binder+0x100003f8e>
100003ee6: bf 01 00 00 00 mov edi, 1
100003eeb: be 02 00 00 00 mov esi, 2
100003ef0: ba 03 00 00 00 mov edx, 3
100003ef5: b9 04 00 00 00 mov ecx, 4
100003efa: 41 b8 05 00 00 00 mov r8d, 5
100003f00: 41 b9 06 00 00 00 mov r9d, 6
100003f06: e8 45 ff ff ff call -187 <__Z10func_6_argiiiiii>
100003f0b: 48 89 df mov rdi, rbx
100003f0e: 89 c6 mov esi, eax
100003f10: e8 79 00 00 00 call 121 <dyld_stub_binder+0x100003f8e>
可以看到在函数调用的过程中没有使用压栈和弹出栈的操作全部使用的寄存器进行参数传递。
再看7个和8个参数传递过程
0000000100003e70 __Z10func_7_argiiiiiii:
100003e70: 55 push rbp
100003e71: 48 89 e5 mov rbp, rsp
100003e74: 8d 04 37 lea eax, [rdi + rsi]
100003e77: 01 d0 add eax, edx
100003e79: 01 c8 add eax, ecx
100003e7b: 44 01 c0 add eax, r8d
100003e7e: 44 01 c8 add eax, r9d
100003e81: 03 45 10 add eax, dword ptr [rbp + 16] // 使用栈参数
100003e84: 5d pop rbp
100003e85: c3 ret
100003e86: 66 2e 0f 1f 84 00 00 00 00 00 nop word ptr cs:[rax + rax]
0000000100003e90 __Z10func_8_argiiiiiiii:
100003e90: 55 push rbp
100003e91: 48 89 e5 mov rbp, rsp
100003e94: 8d 04 37 lea eax, [rdi + rsi]
100003e97: 01 d0 add eax, edx
100003e99: 01 c8 add eax, ecx
100003e9b: 44 01 c0 add eax, r8d
100003e9e: 44 01 c8 add eax, r9d
100003ea1: 03 45 10 add eax, dword ptr [rbp + 16] // 使用栈参数
100003ea4: 03 45 18 add eax, dword ptr [rbp + 24] // 使用栈参数
100003ea7: 5d pop rbp
100003ea8: c3 ret
100003ea9: 0f 1f 80 00 00 00 00 nop dword ptr [rax]
0000000100003eb0 _main:
100003eb0: 55 push rbp
100003eb1: 48 89 e5 mov rbp, rsp
100003eb4: 53 push rbx
100003eb5: 50 push rax
100003f15: 48 83 ec 08 sub rsp, 8 // 移动,这里主要是为了16 byte对齐
100003f19: bf 01 00 00 00 mov edi, 1
100003f1e: be 02 00 00 00 mov esi, 2
100003f23: ba 03 00 00 00 mov edx, 3
100003f28: b9 04 00 00 00 mov ecx, 4
100003f2d: 41 b8 05 00 00 00 mov r8d, 5
100003f33: 41 b9 06 00 00 00 mov r9d, 6
100003f39: 6a 07 push 7 // 参数压栈 8个byte,加上对应的补齐8byte正好16
100003f3b: e8 30 ff ff ff call -208 <__Z10func_7_argiiiiiii>
100003f40: 48 83 c4 10 add rsp, 16 // 调用者恢复栈指针
100003f44: 48 89 df mov rdi, rbx
100003f47: 89 c6 mov esi, eax
100003f49: e8 40 00 00 00 call 64 <dyld_stub_binder+0x100003f8e>
100003f4e: bf 01 00 00 00 mov edi, 1
100003f53: be 02 00 00 00 mov esi, 2
100003f58: ba 03 00 00 00 mov edx, 3
100003f5d: b9 04 00 00 00 mov ecx, 4
100003f62: 41 b8 05 00 00 00 mov r8d, 5
100003f68: 41 b9 06 00 00 00 mov r9d, 6
100003f6e: 6a 08 push 8 // 参数由右向左压栈
100003f70: 6a 07 push 7 // 参数由右向左压栈
100003f72: e8 19 ff ff ff call -231 <__Z10func_8_argiiiiiiii>
100003f77: 48 83 c4 10 add rsp, 16 // 调用者恢复栈指针
100003f7b: 48 89 df mov rdi, rbx
100003f7e: 89 c6 mov esi, eax
100003f80: e8 09 00 00 00 call 9 <dyld_stub_binder+0x100003f8e>
100003f85: 31 c0 xor eax, eax
100003f87: 48 83 c4 08 add rsp, 8
100003f8b: 5b pop rbx
100003f8c: 5d pop rbp
100003f8d: c3 ret
可以看出在超过6个参数的时候就会使用压栈的方式传递参数而出现不足16byte整数的时候会发生补齐的操作。
因此我们在写代码的时候注意不要传递太多参数,因为使用寄存器操作要远快于使用栈操作(可能这个就是叫fast_call的原因吧)。
x86调用规约
x86就有点群魔乱舞的意思了,主要有三种,cdel(C标准), stdcall(巨硬)和fast_call(使用寄存器)。
cdel:参数从右向左入栈,返回值在eax,由调用者(caller)自己去清理,即在被函数调用退出之后由调用者维护栈平衡。
stdcall:参数从右到左入栈,返回值在eax,由被调用的函数(callee)自己清理,即在被调用函数退出前清理堆栈。
fast_call:使用寄存器ecx edx存前两个参数,其余参数从右到左入栈,返回值在eax,由被调用的函数(callee)自己清理,即在被调用函数退出前清理堆栈。