调用约定和栈布局
x86
x86平台下常用的有三种调用约定,__cdecl、__stdcall、__fastcall,默认情况下使用__cdecl。
__cdecl
__cdecl是c/c++使用的默认调用约定。在x86架构下,参数从右到左通过栈传递(右边参数先入栈),由调用者负责清除参数。
返回值通过eax寄存器传递。
下边是一个简单的c程序使用使用如下命令编译结果。
gcc -m32 -O0 -fno-stack-protector -fno-pie callspec_x86.c
int add(int a, int b) {
int c;
c = a + b;
return c;
}
int main() {
return add(2,3);
}
000004ed <add>:
4ed: 55 push %ebp # 保存帧指针
4ee: 89 e5 mov %esp,%ebp
4f0: 83 ec 10 sub $0x10,%esp # 分配局部变量空间
4f3: 8b 55 08 mov 0x8(%ebp),%edx
4f6: 8b 45 0c mov 0xc(%ebp),%eax
4f9: 01 d0 add %edx,%eax
4fb: 89 45 fc mov %eax,-0x4(%ebp)
4fe: 8b 45 fc mov -0x4(%ebp),%eax # 传递返回值
501: c9 leave
502: c3 ret
00000503 <main>:
503: 55 push %ebp
504: 89 e5 mov %esp,%ebp
506: 6a 03 push $0x3
508: 6a 02 push $0x2 # 参数入栈
50a: e8 de ff ff ff call 4ed <add>
50f: 83 c4 08 add $0x8,%esp # 清楚参数
512: c9 leave
513: c3 ret
调用前栈布局,左图为50a
即将执行时,右图为50a
执行后,即call指令压入了一个返回地址。
4ee
执行后(左图)(帧指针保存和移动)以及4f0
执行后(右图)(分配局部变量空间)
4ee
指的是上边汇编代码的地址
501
执行后(右图)(离开当前函数帧)和502
执行后(左图)(返回main函数)。leave
指令作用如下
mov %ebp, %esp
pop %ebp
执行leave
指令后栈指针和帧指针将指向main函数的函数帧,esp指向返回地址,后续执行ret
指令则将返回地址出栈并赋值给eip,
实现返回main函数继续执行。
x86-64
windows
-
整数参数(包括指针)
参数编号(从左边开始) 对应寄存器 参数1 RCX 参数2 RDX 参数3 R8 参数4 R9 返回值 RAX 超过4个参数时,剩下的参数从右往左依次入栈。
-
浮点数参数
参数编号(从左边开始) 对应寄存器 参数1 xmm0 参数2 xmm1 参数3 xmm2 参数4 xmm3 返回值 xmm0
- 编译器验证
测试使用的c代码如下:
int add(int a, int b, int c, int d) {
int e;
e = a + b + c + d;
return e;
}
float add_float(float a, float b, float c, float d) {
float e;
e = a + b + c + d;
return e;
}
int main() {
add(1,2,3,4);
add_float(1.0, 2.0, 3.0, 4.0);
return 0;
}
编译后(使用MinGW-w64)的main函数(下边的r9d是r9寄存器的低32位,r8d同理)
00000000004015c8 <main>:
4015c8: 55 push %rbp
4015c9: 48 89 e5 mov %rsp,%rbp
4015cc: 48 83 ec 20 sub $0x20,%rsp
4015d0: e8 fb 00 00 00 callq 4016d0 <__main>
4015d5: 41 b9 04 00 00 00 mov $0x4,%r9d
4015db: 41 b8 03 00 00 00 mov $0x3,%r8d
4015e1: ba 02 00 00 00 mov $0x2,%edx
4015e6: b9 01 00 00 00 mov $0x1,%ecx
4015eb: e8 60 ff ff ff callq 401550 <add>
4015f0: f3 0f 10 1d 08 2a 00 movss 0x2a08(%rip),%xmm3 # 404000 <.rdata>
4015f7: 00
4015f8: f3 0f 10 15 04 2a 00 movss 0x2a04(%rip),%xmm2 # 404004 <.rdata+0x4>
4015ff: 00
401600: f3 0f 10 0d 00 2a 00 movss 0x2a00(%rip),%xmm1 # 404008 <.rdata+0x8>
401607: 00
401608: f3 0f 10 05 fc 29 00 movss 0x29fc(%rip),%xmm0 # 40400c <.rdata+0xc>
40160f: 00
401610: e8 6f ff ff ff callq 401584 <add_float>
401615: b8 00 00 00 00 mov $0x0,%eax
40161a: 48 83 c4 20 add $0x20,%rsp
40161e: 5d pop %rbp
40161f: c3 retq
Linux(System V AMD64 ABI)
-
整数参数(包括指针)
整数参数编号 对应寄存器 参数1 rdi 参数2 rsi 参数3 rdx 参数4 rcx 参数5 r8 参数6 r9 返回值 rax(64位), rax+rdx(128位) 超过6个参数后通过栈传递。
-
浮点数参数
浮点数参数编号 对应寄存器 参数1 xmm0 参数2 xmm1 参数3 xmm2 参数4 xmm3 参数5 xmm4 参数6 xmm5 参数7 xmm6 参数8 xmm7 返回 xmm0, xmm1 超过8个参数后通过栈传递。
- 编译器测试
测试c源代码同上一小节相同,使用如下命令编译
gcc call_convention.c -o call_convention
使用objdump查看main函数汇编代码
0000000000000660 <main>:
660: 55 push %rbp
661: 48 89 e5 mov %rsp,%rbp
664: b9 04 00 00 00 mov $0x4,%ecx
669: ba 03 00 00 00 mov $0x3,%edx
66e: be 02 00 00 00 mov $0x2,%esi
673: bf 01 00 00 00 mov $0x1,%edi
678: e8 7d ff ff ff callq 5fa <add>
67d: f3 0f 10 1d af 00 00 movss 0xaf(%rip),%xmm3 # 734 <_IO_stdin_used+0x4>
684: 00
685: f3 0f 10 15 ab 00 00 movss 0xab(%rip),%xmm2 # 738 <_IO_stdin_used+0x8>
68c: 00
68d: f3 0f 10 0d a7 00 00 movss 0xa7(%rip),%xmm1 # 73c <_IO_stdin_used+0xc>
694: 00
695: f3 0f 10 05 a3 00 00 movss 0xa3(%rip),%xmm0 # 740 <_IO_stdin_used+0x10>
69c: 00
69d: e8 82 ff ff ff callq 624 <add_float>
6a2: b8 00 00 00 00 mov $0x0,%eax
6a7: 5d pop %rbp
6a8: c3 retq
6a9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
附录
如果不加-fno-pie
编译则gcc会生成位置无关的代码(可以加载到任意位置执行),在x86架构下
会调用__x86.get_pc_thunk.ax
函数将eip的值保存在eax中实现基于eip的偏移寻址,如下汇编代码所示。
在x86-64架构下,由于指令集支持pc偏移寻址,因此就不需要上述函数了。
gcc -m32 -O0 -fno-stack-protector callspec_x86.c
000004ed <add>:
4ed: 55 push %ebp
4ee: 89 e5 mov %esp,%ebp
4f0: 83 ec 10 sub $0x10,%esp
4f3: e8 30 00 00 00 call 528 <__x86.get_pc_thunk.ax>
4f8: 05 e4 1a 00 00 add $0x1ae4,%eax
4fd: 8b 55 08 mov 0x8(%ebp),%edx
500: 8b 45 0c mov 0xc(%ebp),%eax
503: 01 d0 add %edx,%eax
505: 89 45 fc mov %eax,-0x4(%ebp)
508: 8b 45 fc mov -0x4(%ebp),%eax
50b: c9 leave
50c: c3 ret
0000050d <main>:
50d: 55 push %ebp
50e: 89 e5 mov %esp,%ebp
510: e8 13 00 00 00 call 528 <__x86.get_pc_thunk.ax>
515: 05 c7 1a 00 00 add $0x1ac7,%eax
51a: 6a 03 push $0x3
51c: 6a 02 push $0x2
51e: e8 ca ff ff ff call 4ed <add>
523: 83 c4 08 add $0x8,%esp
526: c9 leave
527: c3 ret