C语言 函数调用模型

简介: 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))。


调用管理表


image.png


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函数均可以使用



这里就不给大家一一举例子了,大家有兴趣可以试一下

相关文章
|
存储 编译器 程序员
C语言的模型玩具:结构体的使用以及操作符优先级
C语言的模型玩具:结构体的使用以及操作符优先级
178 1
|
网络协议 安全 网络安全
C语言 网络编程(四)常见网络模型
这段内容介绍了目前被广泛接受的三种网络模型:OSI七层模型、TCP五层模型以及TCP/IP四层模型,并简述了多个网络协议的功能与特性,包括HTTP、HTTPS、FTP、DNS、SMTP、TCP、UDP、IP、ICMP、ARP、RARP及SSH协议等,同时提到了ssh的免费开源实现openssh及其在Linux系统中的应用。
|
网络协议 数据处理 C语言
利用C语言基于poll实现TCP回声服务器的多路复用模型
此代码仅为示例,展示了如何基于 `poll`实现多路复用的TCP回声服务器的基本框架。在实际应用中,你可能需要对其进行扩展或修改,以满足具体的需求。
360 0
|
机器学习/深度学习 算法 语音技术
llama.cpp作者创业,用纯C语言框架降低大模型运行成本
llama.cpp作者创业,用纯C语言框架降低大模型运行成本
1387 0
|
C语言
【C 语言】二级指针内存模型 ( 指针数组 | 二维数组 | 自定义二级指针 | 将 一、二 模型数据拷贝到 三 模型中 并 排序 )
【C 语言】二级指针内存模型 ( 指针数组 | 二维数组 | 自定义二级指针 | 将 一、二 模型数据拷贝到 三 模型中 并 排序 )
328 0
【C 语言】二级指针内存模型 ( 指针数组 | 二维数组 | 自定义二级指针 | 将 一、二 模型数据拷贝到 三 模型中 并 排序 )
|
C语言
【C 语言】字符串模型 ( 键值对模型 )
【C 语言】字符串模型 ( 键值对模型 )
244 0
【C 语言】字符串模型 ( 键值对模型 )
|
存储 安全 C语言
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
192 0
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
|
人工智能 C语言
【C 语言】数组 ( 多维数组操作模型 | 取某个数组元素地址 | 取某个数组元素值 )
【C 语言】数组 ( 多维数组操作模型 | 取某个数组元素地址 | 取某个数组元素值 )
268 0
|
存储 Java C语言
【C 语言】一级指针 易犯错误 模型 ( 判定指针合法性 | 数组越界 | 不断修改指针变量值 | 函数中将栈内存数组返回 | 函数间接赋值形参操作 | 指针取值与自增操作 )
【C 语言】一级指针 易犯错误 模型 ( 判定指针合法性 | 数组越界 | 不断修改指针变量值 | 函数中将栈内存数组返回 | 函数间接赋值形参操作 | 指针取值与自增操作 )
321 0