- 上一篇文章学习了Linux环境下的函数栈帧的形成与摧毁。点击链接查看相关文章:软件开发底层知识修炼】二十三 ABI-应用程序二进制接口三之深入理解函数栈帧的形成与摧毁
- 本篇文章继续学习ABI接口相关的内容。函数调用约定
1 函数参数如何入栈,返回值在哪里
前面学过的文章中,已经深入的了解了函数调用过程中函数的栈帧的形成与摧毁。在发生函数调用时,首先入栈的是函数的参数。但是我们知道一般函数的参数都是会比较多。在参数比较多的时候,函数的参数是以什么样的顺序入栈的呢?函数返回时是谁来将参数弹出栈呢?
并且,在函数执行完之后,返回的时候,返回值在哪里?通过什么方式将返回值传递给调用者?
首先给出在C语言中默认的调用约定(cdecl):
- 调用函数时,参数从右往左入栈
- 函数返回时,调用者负责将参数弹出栈。这里说是调用者,实际上是编译器在编译的时候为调用者添加了相应的指令
- 函数返回值保存在eax寄存器中。前提是函数返回值是基础数据类型,如果是结构体这种的类型,就另说。
上面的调用约定是C语言默认的函数调用约定。我们平常所熟知的也就是上面的默认的调用约定。当然,或许大多数人是不知道的吧!
- 下面的表格给出了其他各种调用约定
注意:以上三个调用约定,只需要注意
__thiscall__
约定。它一般是C++中的成员函数的约定,C++成员函数又由隐藏的this指针。如果函数的参数是确定个数的,则this存放于ECX寄存器,函数自身清理栈中的参数。如果成员函数是可变参数,那么久相当于前面的__cdecl__
调用约定
在上面的函数调用约定中,还有几点需要注意:
- 只有使用的
__cdecl__
约定的函数,才支持可变参数。使用其他调用约定的,不支持可变参数 - 在C++中,当类的成员函数为可变参数时,调用约定自动变为
__cedcl__
- 调用约定定义义了函数被编译后,对应的在符号表中的最终的符号名称的样子
2 函数调用约定的编程实验
2.1 使用gdb调试代码证明eax存的值是函数返回值
convention.c
#include <stdio.h>
int test(int a, int b, int c) //默认的__cdecl__调用约定
{
return a + b + c;
}
//__cdecl__调用约定
void __attribute__((__cdecl__)) func_1(int i)
{
}
//__stdcall__调用约定
void __attribute__((__stdcall__)) func_2(int i)
{
}
//__fastcall__调用约定
void __attribute__((__fastcall__)) func_3(int i)
{
}
int main()
{
int r = test(1, 2, 3);
printf("r = %d\n", r);
return 0;
}
- 上述代码很简单,分别将几个函数强制设定为相应的调用约定,并可以通过查看变量r的内容与寄存器eax的内容来证明函数返回值是存储在eax寄存器中的。下面就来通过运行该程序,并使用GDB进行查看相应的内存的值。
编译运行程序,并且记性gdb调试
- gcc -g convention.c -o test.out
- gdb test.out
- start
- break convention.c:6
- continue
- info registers
- continue
因为gdb在前面的文章中已经使用过很多次,并且使用了各种动态图展示gdb的使用。这里就直接给出相应的命令了。
上述第一个contiue后,运行到convention.c的第6行就停止了。此时再info registers。可以看到如下信息:
可以看到在函数test即将返回时。寄存器eax存的值为6 。 这正好等于test函数的返回值这不是巧合。因为eax寄存器就是用来保存函数的返回值的。当然,后面我们还可以通过查看反汇编文件来分析。
最后的执行continue命令导致整个程序的运行结束
2 .2 查看程序的反汇编文件来说明调用约定的不同
上面的gdb调试方法没有证明出函数调用约定的不同,下面我么使用查看可执行代码的反汇编文件进行说明。
使用命令
- objdump -S test.c > test.s
生成可执行文件的反汇编代码test.s。
在该文件中可以找到
- func_1函数的反汇编代码
void __attribute__((__cdecl__)) func_1(int i)
{
80483d5: 55 push %ebp
80483d6: 89 e5 mov %esp,%ebp
}
80483d8: 5d pop %ebp
80483d9: c3 ret
- func_2的反汇编代码
080483da <func_2>:
void __attribute__((__stdcall__)) func_2(int i)
{
80483da: 55 push %ebp
80483db: 89 e5 mov %esp,%ebp
}
80483dd: 5d pop %ebp
80483de: c2 04 00 ret $0x4
- func_3的反汇编代码
080483e1 <func_3>:
void __attribute__((__fastcall__)) func_3(int i)
{
80483e1: 55 push %ebp
80483e2: 89 e5 mov %esp,%ebp
80483e4: 83 ec 04 sub $0x4,%esp
80483e7: 89 4d fc mov %ecx,-0x4(%ebp)
}
80483ea: c9 leave
80483eb: c3 ret
- 从以上三个函数的反汇编代码可以看出,它们的前言和后序是由区别的,这是因为它们分别使用了不同的函数调用约定。使用了不同的 函数调用预定,汇编层面肯定是不一样的。
3 总结
学会了以下内容
- 函数返回值如果是整形的,通过eax寄存器传递返回值给调用者(下一篇文章讲解返回值是结构体的类型个时是如何传递给调用者的)
- 学会三种不同的调用约定对应的区别(
__cdecl__
调用约定,__stdcall__
调用约定,__fastcall__
调用约定)