C++ 调用规约

907 阅读7分钟

最近在学习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)自己清理,即在被调用函数退出前清理堆栈。