Calling Conventions 调用惯例

Calling Conventions 调用惯例

摘自维基, 暂时先搬运, 以后会加入自己的理解(又挖坑了 QWQ)

调用惯例涉及以下几点:

1. 函数参数的空间分配顺序

2. 这些参数存储在什么地方(是全部放在栈上,还是部分放在栈上,部分放在寄存器里,还是全部放在寄存器里)

3.被调用的函数需要保留哪些寄存器给调用他的函数

4.调用函数的时候, 保护现场, 恢复现场的顺序.

 __cdecl:
1> 参数按从右到左的顺序传递,放于栈中
2> 栈的清空由主调函数完成

 __stdcall:
    1> 参数按从右到左的顺序传递,放于栈中
2> 栈的清空由被调函数完成

 __fastcall:
1> 前两个参数要求不超过32bits,分别放入ECX和EDX,其余参数按从右到左的顺序传递,放于栈中
2> 参数由被调函数弹出栈

Thiscall,仅用于C++中类的成员函数:
1> 参数按从右到左的顺序传递,放于栈中。this放于ECX中
2> 栈的清空有被调函数完成

下面通过对cdecl 和 stdcall Calling convention 的区别的研究, 学习calling convention的相关知识

cdecl 是 C语言默认的调用惯例 , 也就是说, 如果你什么不加, default为cdecl ,这个调用惯例 , 要求caller清理堆栈, callee不用清理堆栈 , 用cdecl可以实现可变参数的函数, 另一种方式 stdcall, 是由 callee清理堆栈 , caller不负责堆栈的清理 ,下面是两个不同的调用的C代码及汇编   (注意, 在gcc中 想指定Calling Convention 需要用这样的形式 , 以 stdcall为例: __attribute__((__stdcall__)) , 另外注意, 想要看到Calling Convention的效果要将代码编译为 32位的程序而不是64位 , 提供一个编译指令 gcc -m32 -g -O0 xxx.c -o xxx.out 

call.h

#define CDECL __attribute__((__cdecl__))
#define STDCALL __attribute__((__stdcall__))

stdcall.c

#include "call.h"

int STDCALL add(int a, int b)
{
    return a + b;
}

int main(void)
{
    add(1, 2);
    return 0;
}

stdcall.s(part)

080483bb <add>:
#include "call.h"

int STDCALL add(int a, int b)
{
 80483bb:	55                   	push   %ebp
 80483bc:	89 e5                	mov    %esp,%ebp
    return a + b;
 80483be:	8b 55 08             	mov    0x8(%ebp),%edx
 80483c1:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483c4:	01 d0                	add    %edx,%eax
}
 80483c6:	5d                   	pop    %ebp
 80483c7:	c2 08 00             	ret    $0x8

080483ca <main>:

int main(void)
{
 80483ca:	55                   	push   %ebp
 80483cb:	89 e5                	mov    %esp,%ebp
    add(1, 2);
 80483cd:	6a 02                	push   $0x2
 80483cf:	6a 01                	push   $0x1
 80483d1:	e8 e5 ff ff ff       	call   80483bb <add>
    return 0;
 80483d6:	b8 00 00 00 00       	mov    $0x0,%eax
}
 80483db:	c9                   	leave  
 80483dc:	c3                   	ret    
 80483dd:	66 90                	xchg   %ax,%ax
 80483df:	90                   	nop

这里

 80483c7:	c2 08 00             	ret    $0x8

显示出来了, 是由 add函数清理的堆栈 : ret $0x8 表明了是callee (即 add函数) 清理了堆栈,  在主函数没有清理堆栈 ,

另外一段代码, 只把这个代码里的STDCALL换成CDECL, 然后得到的汇编代码如下:

#include "call.h"

int CDECL add(int a, int b)
{
 80483bb:	55                   	push   %ebp
 80483bc:	89 e5                	mov    %esp,%ebp
    return a + b;
 80483be:	8b 55 08             	mov    0x8(%ebp),%edx
 80483c1:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483c4:	01 d0                	add    %edx,%eax
}
 80483c6:	5d                   	pop    %ebp
 80483c7:	c3                   	ret    

080483c8 <main>:

int main(void)
{
 80483c8:	55                   	push   %ebp
 80483c9:	89 e5                	mov    %esp,%ebp
    add(1, 2);
 80483cb:	6a 02                	push   $0x2
 80483cd:	6a 01                	push   $0x1
 80483cf:	e8 e7 ff ff ff       	call   80483bb <add>
 80483d4:	83 c4 08             	add    $0x8,%esp
    return 0;
 80483d7:	b8 00 00 00 00       	mov    $0x0,%eax
}
 80483dc:	c9                   	leave  
 80483dd:	c3                   	ret    
 80483de:	66 90                	xchg   %ax,%ax

这里

80483d4:	83 c4 08             	add    $0x8,%esp

是在主函数对堆栈进行清理的

那么现在就可以猜测,如果我在调用一个函数的时候使用不同的Calling Convention会导致程序异常么? 通过cdecl 和stdcall的对堆栈的处理, 做出如下猜想:

如果caller 用STDCALL的形式调用 callee 声明为CDECL的形式, 那么由于STDCALL将清理堆栈的任务交给callee, CDECL将清理堆栈的任务交给caller, 那么就导致 caller 和 callee谁都没有清理堆栈, 反过来 ,如果caller是CDECL callee 是 STDCALL 那么这样, 就导致 caller 和 callee 都对堆栈进行了清理, 也就是堆栈被重复释放了两次 下面通过代码来验证这个猜想

* PS: 这里如何实现 Convention的混合使用,有一定技巧,我采用的方式是函数指针, 先定义一个函数A, 用ConventionA ,再定义一个函数指针B指向A, 不过B的Convention和A的不同 代码如下:

sample: caller is CDECL callee is STDCALL

#include<stdio.h>
#include "call.h"
int pre_esp;
int now_esp;
int pre_ebp;
int now_ebp;
int STDCALL add(int a, int b)
{
    return a + b;
}

int main(void)
{
    int (CDECL *fun)(int a, int b) = (void *)add;
    int res;
    int x = 1, y = 2;
    asm ("mov %%esp, %0nt" : "=r" (pre_esp) : );
    asm ("mov %%ebp, %0nt" : "=r" (pre_ebp) : );
    //for(int i = 0; i < 10; i++)
    //for(int i = 0; i < 10; i++)
        res = fun(x, y);
    asm ("mov %%esp, %0nt" : "=r" (now_esp) : );
    asm ("mov %%ebp, %0nt" : "=r" (now_ebp) : );
    printf("ESP: pre = 0x%X now = 0x%Xn", pre_esp, now_esp);
    printf("ESP: delta is %dn", now_esp - pre_esp);

    printf("EBP: pre = 0x%X now = 0x%Xn", pre_ebp, now_ebp);
    printf("EBP: delta is %dn", now_ebp - pre_ebp);
    return 0;
}

可以看出, 我们定义了函数add 并且声明了函数指针 fun指向add, 然后在main里调用了fun, 为了保证结果的直观性, 这里使用了内联汇编,  将esp ebp在函数调用前后的值都保存下来了, 作为对比 内联汇编的格式可以在这里找到 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html 然后, 运行程序, 观察结果, 因为每次程序初始化的时候esp, ebp指向的地址有所不同, 但是调用前后, esp位置应该是相同的, 运行程序得到的一个可能结果如下

ESP: pre = 0xFFDB31D0 now = 0xFFDB31D8
ESP: delta is 8
EBP: pre = 0xFFDB31E8 now = 0xFFDB31E8
EBP: delta is 0

注意看, 这里nowesp 和 preesp 差了8 , 这个意味什么稍后再说, 我们再将CDECL 和 STDCALL的位置互换 , 重新编译运行 得到的结果如下:

ESP: pre = 0xFFFF1020 now = 0xFFFF1018
ESP: delta is -8
EBP: pre = 0xFFFF1038 now = 0xFFFF1038
EBP: delta is 0

我们知道 在IA32程序中, 栈的增长是向低地址增长的, 也就是说, 压栈会导致esp减小, 那么 我们来解释一下之前的现象, 在第一次试验中, esp最终增加了 8 也就意味着, esp被多清理了一次, caller清理好堆栈之后, callee又清理了一次, 导致esp最后”错位”了8个字节, 而第二次试验, esp最后减少了8, 这就是二者都没有清理堆栈的后果, 栈内还存着传过去的参数的信息, 这样两周情况, 都可能导致程序崩溃, 也就是ABI不兼容导致的程序崩溃

给add函数增加一个参数int c 然后修改下面的函数指针和函数调用之后, 再次运行代码, 发现esp最后差12, 也进一步说明了, 这个esp的异常值是因为没有清理堆栈内的参数或者二次清理堆栈内的参数表导致的

另外 如果将程序里两个Calling Convention替换成一样的, 不论是CDECL还是都是STDCALL, 那么 esp在调用前后都是保持不变的,

不过之前写的代码都没有引起程序崩溃, 下面这段代码做到了使程序崩溃, 然后之要将 Calling Convention改为一样的, 就避免掉了崩溃 , 这段代码为什么会让它崩溃的原因还在研究, 个人猜测是因为: 循环中的这个计数变量被存在了栈内, 因此由于每次调用函数栈都会错位, 最后这个i就出问题了

#define STDCALL __attribute__((__stdcall__))
#define CDECL __attribute__((__cdecl__))
int STDCALL add(int a, int b, int c)
{
    return a + b;
}

int main(void)
{
    int (CDECL *fun)(int a, int b, int c) = (void *)add;
    int res;
    int x = 1, y = 2;
    for(int i = 0; i < 10; i++)
        res = fun(x, y, x);
    return 0;
}

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

18 − 15 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.