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__调用约定)