调用约定

调用约定(Calling Conventions)指的是为了函数调用能顺利进行,被调用者与调用者达成的一系列包括参数传递与名字修饰在内的约定。

总的来说有下面几种:

  1. 参数传递约定
  • 极微参数或复杂参数独立部分的分配顺序
  • 参数是如何被传递的(放置在堆栈上,或是寄存器中,亦或两者混合)
  • 被调用者应保存调用者的哪个寄存器
  • 调用函数时如何为任务准备堆栈,以及任务完成如何恢复
  1. 名字修饰约定

x86架构微处理器有多种调用约定(Calling Conventions),不同语言,甚至不同的编译器都有不同的编译选项以便开发者选择。出于方便,本文仅从GCC的角度进行分析。

参数传递约定

为了能更直观的观察各个不同调用约定产生的汇编代码的区别,需要关闭编译优化,同时也不需要去搜索标准系统目录,我们选择如下编译选项:

gcc -march=i686 -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc

cdelcall

C声明调用
cdel(C declaration)即C声明,是C语言实际上的默认标准。主要约定如下:

  1. 函数实参从右至左依次压栈
  2. 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
  3. 调用结束后,栈数据不会清理,调用者需按自己需要进行处理。

所编译的代码:

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

该调用过程分为以下三步:

  1. 先看调用者main函数,由于3个int长度为12字节,调用者在栈上开辟了12字节的空间(ESP - 0xC),然后依次存入3, 2, 1。接着调用_Z7stdfunciii。
  2. 被调用者_Z7stdfunciii将调用者的EBP压栈,然后从栈上0x8, 0xC, 0x10分别取出参数,运算后存入EAX。
  3. 第三步为隐藏操作,由于函数使用指令ret返回,CPU不会对栈进行任何处理,栈上依然保留着之前的参数,此时应由调用者进行手动处理。

stdcall

标准调用

  1. 函数实参从右至左依次压栈
  2. 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
  3. 调用结束后,栈数据将会由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

快速调用

  1. 函数前两个参数通过寄存器ECX与EDX传递,其他参数从左至右通过栈传递
  2. 函数结果保存在EAX中(16位及8位程序分别为AX, AL)
  3. 调用结束后,栈数据将会由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这个名字。

最后修改:2020 年 10 月 27 日 01 : 32 AM
如果觉得我的文章对你有用,请随意赞赏