调用约定
调用约定(Calling Conventions)指的是为了函数调用能顺利进行,被调用者与调用者达成的一系列包括参数传递与名字修饰在内的约定。
总的来说有下面几种:
- 参数传递约定
- 极微参数或复杂参数独立部分的分配顺序
- 参数是如何被传递的(放置在堆栈上,或是寄存器中,亦或两者混合)
- 被调用者应保存调用者的哪个寄存器
- 调用函数时如何为任务准备堆栈,以及任务完成如何恢复
- 名字修饰约定
x86架构微处理器有多种调用约定(Calling Conventions),不同语言,甚至不同的编译器都有不同的编译选项以便开发者选择。出于方便,本文仅从GCC的角度进行分析。
参数传递约定
为了能更直观的观察各个不同调用约定产生的汇编代码的区别,需要关闭编译优化,同时也不需要去搜索标准系统目录,我们选择如下编译选项:
gcc -march=i686 -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc
cdelcall
C声明调用
cdel(C declaration)即C声明,是C语言实际上的默认标准。主要约定如下:
- 函数实参从右至左依次压栈
- 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
- 调用结束后,栈数据不会清理,调用者需按自己需要进行处理。
所编译的代码:
int __attribute__((__cdelcall__)) cdelfunc(int x, int y, int z) { return x + y + z; }
int main() { cdelfunc(1, 2, 3); }
经过objdump -S
可以比较得到如下的汇编代码(无关代码已经忽略)
000011ad <_Z8cdelfunciii>:
int __attribute__((__cdelcall__)) cdelfunc(int x, int y, int z) { return x + y + z; }
11b1: 55 push %ebp
11b2: 89 e5 mov %esp,%ebp
11b4: 8b 55 08 mov 0x8(%ebp),%edx
11b7: 8b 45 0c mov 0xc(%ebp),%eax
11ba: 01 c2 add %eax,%edx
11bc: 8b 45 10 mov 0x10(%ebp),%eax
11bf: 01 d0 add %edx,%eax
11c1: 5d pop %ebp
11c2: c3 ret
000011c3 <main>:
11c7: 55 push %ebp
11c8: 89 e5 mov %esp,%ebp
11ca: 83 ec 0c sub $0xc,%esp
11cd: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
11d5: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
11dd: c7 04 24 01 00 00 00 movl $0x1,(%esp)
11e4: e8 c4 ff ff ff call 11ad <_Z8cdelfunciii>
11e9: b8 00 00 00 00 mov $0x0,%eax
该调用过程分为以下三步:
- 先看调用者main函数,由于3个int长度为12字节,调用者在栈上开辟了12字节的空间(ESP - 0xC),然后依次存入3, 2, 1。接着调用_Z7stdfunciii。
- 被调用者_Z7stdfunciii将调用者的EBP压栈,然后从栈上0x8, 0xC, 0x10分别取出参数,运算后存入EAX。
- 第三步为隐藏操作,由于函数使用指令ret返回,CPU不会对栈进行任何处理,栈上依然保留着之前的参数,此时应由调用者进行手动处理。
stdcall
标准调用
- 函数实参从右至左依次压栈
- 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
- 调用结束后,栈数据将会由CPU进行清理。
类似的,我们使用__stdcall__替换__cdelcall__,将会得到如下的汇编代码:
000011ad <_Z7stdfunciii>:
int __attribute__((__stdcall__)) stdfunc(int x, int y, int z) { return x + y + z; }
11ad: f3 0f 1e fb endbr32
11b1: 55 push %ebp
11b2: 89 e5 mov %esp,%ebp
11b4: 8b 55 08 mov 0x8(%ebp),%edx
11b7: 8b 45 0c mov 0xc(%ebp),%eax
11ba: 01 c2 add %eax,%edx
11bc: 8b 45 10 mov 0x10(%ebp),%eax
11bf: 01 d0 add %edx,%eax
11c1: 5d pop %ebp
11c2: c2 0c 00 ret $0xc
000011c5 <main>:
11c5: f3 0f 1e fb endbr32
11c9: 55 push %ebp
11ca: 89 e5 mov %esp,%ebp
11cc: 83 ec 0c sub $0xc,%esp
11cf: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
11d7: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
11df: c7 04 24 01 00 00 00 movl $0x1,(%esp)
11e6: e8 c2 ff ff ff call 11ad <_Z7stdfunciii>
11eb: 83 ec 0c sub $0xc,%esp
11ee: b8 00 00 00 00 mov $0x0,%eax
可以发现,stdcall与cdelcall最大的不同的是函数使用ret 0xC返回。
这条指令的含义是:通知CPU清理栈上的0xC数据(也就是CPU会自行帮你进行ESP + 0xC的操作)
fastcall
快速调用
- 函数前两个参数通过寄存器ECX与EDX传递,其他参数从左至右通过栈传递
- 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
- 调用结束后,栈数据将会由CPU进行清理。
000011ad <_Z8fastfunciii>:
int __attribute__((__fastcall__)) fastfunc(int x, int y, int z) { return x + y + z; }
11b1: 55 push %ebp
11b2: 89 e5 mov %esp,%ebp
11b4: 83 ec 08 sub $0x8,%esp
11b7: 89 4d fc mov %ecx,-0x4(%ebp)
11ba: 89 55 f8 mov %edx,-0x8(%ebp)
11bd: 8b 55 fc mov -0x4(%ebp),%edx
11c0: 8b 45 f8 mov -0x8(%ebp),%eax
11c3: 01 c2 add %eax,%edx
11c5: 8b 45 08 mov 0x8(%ebp),%eax
11c8: 01 d0 add %edx,%eax
11ca: c9 leave
11cb: c2 04 00 ret $0x4
000011ce <main>:
11d2: 55 push %ebp
11d3: 89 e5 mov %esp,%ebp
11d5: 83 ec 04 sub $0x4,%esp
11d8: c7 04 24 03 00 00 00 movl $0x3,(%esp)
11df: ba 02 00 00 00 mov $0x2,%edx
11e4: b9 01 00 00 00 mov $0x1,%ecx
11e9: e8 bf ff ff ff call 11ad <_Z8fastfunciii>
11ee: 83 ec 04 sub $0x4,%esp
11f1: b8 00 00 00 00 mov $0x0,%eax
fastcall与cdelcall有了比较大的区别,该调用使用了ECX与EDX两个寄存器进行前两个参数的传递,只将第三个参数压栈。
寄存器操作相比栈操作(栈的本质是主存)存在指数级的速度差别。
在只有1-2个参数,且函数内部指令较少时,该调用能带来较为可观的速度优化,也算对得起fast这个名字。