C语言 函数调用模型

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言 函数调用模型相关的知识,希望对你有一定的参考价值。

1. 函数调用流程

       栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。在解释为什么栈如此重要之前,我们先了解一下传统的栈的定义:

       在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).

       在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。

栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record)。

一个函数调用过程所需要的信息一般包括以下几个方面:

  • 函数的返回地址;
  • 函数的参数;
  • 临时变量;
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

函数调用流程分析

函数被调用的过程中,发生了如下图的栈操作:

从上图我们可以看到:C语言函数参数采用自右向左的入栈顺序(主要原因是为了支持可变长参数形式);当被调用函数返回时,以上压入栈中的所有空间都会被回收。

函数参数调用代码分析

大家可以运行一下下面的例子:

#include <stdio.h>

void foo(int x, int y, int z)
{
        printf("x = %d at [%X]/n", x, &x);
        printf("y = %d at [%X]/n", y, &y);
        printf("z = %d at [%X]/n", z, &z);
}

int main(int argc, char *argv[])
{
        foo(100, 200, 300);
        return 0;
}


他的运行结果是:

x = 100 at [BFE28760]
y = 200 at [BFE28764]
z = 300 at [BFE28768]

我们可以看到,Z的地址是最大的,上文我们也提到了:C程序栈底为高地址,栈顶为低地址,因此上面的实例可以说明函数参数入栈顺序的确是从右至左的。

自右向左入栈顺序的优点

C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数

通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

2. 调用惯例

现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。

如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。

因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例(Calling Convention)”。

一个调用惯例一般包含以下几个方面:

函数参数的传递顺序和方式

函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。

对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。

栈的维护方式

在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。

为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是:

int _cdecl func(int a,int b);

注意: _cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute__((cdecl))

调用管理表

调用惯例出栈方参数传递名字修饰
cdecl函数调用方从右至左参数入栈下划线+函数名
stdcall函数本身从右至左参数入栈下划线+函数名+@+参数字节数
fastcall函数本身前两个参数由寄存器传递,其余参数通过堆栈传递。@+函数名+@+参数的字节数
pascal函数本身从左至右参数入栈较为复杂,参见相关文档

3. 函数变量传递分析

可能大家会有疑问:主函数里还能嵌套函数呢?

在Linux下确实是可以的,但是在VS环境里就会报错。

大家可以看下面的例子:

他在gcc编译下运行下是这样的

#include <stdio.h>

int main(int argc, char *argv[])
{
	int fun(void)
	{
		printf("fun in main\\n");
	}
	fun();
	return 0;
}

分析图

这里补一下栈区,堆区,全局区存储的数据类型知识:
栈区:由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。
堆区:由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
全局/静态区:全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。

1.main函数在栈区开辟的内存,所有子函数均可以使用。

2.main函数在堆区开辟的内存,所有子函数均可以使用。

3.子函数1在栈区开辟的内存,子函数1和子函数2均可以使用,main函数不能使用。

4.子函数1在栈区开辟的内存,子函数1和2均可以使用,main函数也能使用。

5.子函数2在全局区开辟的内存,子函数1和main函数均可以使用

这里就不给大家一一举例子了,大家有兴趣可以试一下,留言大家一起探讨 Thanks♪(・ω・)ノ

以上是关于C语言 函数调用模型的主要内容,如果未能解决你的问题,请参考以下文章

php 一个自定义的try..catch包装器代码片段,用于执行模型函数,使其成为一个单行函数调用

VBS 环境下如何调用EXCEL内置函数

[架构之路-24]:目标系统 - 系统软件 - C语言的结构与程序的工作原理 - 程序控制函数调用栈函数调用性能优化

C 语言字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )

c语言函数的调用和声明

C 语言字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序打印字符串 | 递归要素 | 递归停止条件 | 递归操作 )