C语言函数调用过程

一个(过程或函数)调用顺序 就是调用一个过程并从该过程返回的指令的约定顺序。由于一个程序是很多的函数的调用来实现的,所以编译器应该使这个调用的顺序尽可能的经济划算。函数调用顺序受语言语义、寄存器的规划、寻址模式、指令集和目标机器的固有约定等多方面的影响。函数调用顺序还与局部变量的内存分配方式有着紧密的联系。

Introduction

原文

一个(过程或函数)调用顺序就是调用一个过程并从该过程返回的指令的约定顺序。由于一个程序是很多的函数的调用来实现的,所以编译器应该使这个调用的顺序尽可能的经济划算。

函数调用顺序受语言语义、寄存器的规划、寻址模式、指令集和目标机器的固有约定等多方面的影响。函数调用顺序还与局部变量的内存分配方式有着紧密的联系。

这个文档设定了四个在为C语言设计函数调用顺序时需要考虑的问题,并且讨论在不同的环境下的一些经验。系统实现者和硬件设计者会从关于C的有效支持中得到一些提示。在最初的用户看来,用今天的硬件尽管实现一种简单的语言对他们来说也是相当难以想像的。本文档的头几部分我们讨论C语言的函数调用和返回的强制约定,包括递归的支持、可变大小参数列表和(可能被函数调用方忽略的)不同类型返回值的返回方法。我们会举一些设计的例子。最后我们讨论另外相关不同的内容,比如堆栈的增长、结构值的过程支持和库函数setjmp and longjmp 。

The Basic Issues

调用一个C函数能提供什么样的功能呢?函数可能不用显式的声明就可递归地调用自己。由于函数可以把本地局部变量或参数的地址传给子函数,因此为局部变量分配固定内存几乎不可能。所以,C运行时环境创建堆栈来存放函数的参数和局部变量。由于程序的动态数据是按需分配的,所以程序的堆栈最大所需空间是很难事先估计的。因此使用堆栈潜规则就是小用,按需增长,并且在堆栈溢出时有相应的处理机制。

C语言的定义并没有对函数的具体实现有过多的规定,比如参数的个数或类型。但是作为一个具体的现实,这些问题是要被处理的。

通常只有函数调用方才知道被传递的参数的个数,而被调用方不知道(kemin:是这样的吗?不是两都知道的吗?);另一方面,只有被调用方知道函数会使用多少个局部变量,而调用方不知道。这种参数个数可变的函数调用方式在一些机器的函数顺序设计占主导地位。通常支持过程调用的硬件都假定参数的个数是调用方和被调用方都知道的。

基本的调用返回过程

The following things happen during a C call and return:

  1. 调用前参数被评估并放到一个默认的地方;
  2. 返回地址压进堆栈;
  3. 控制权转给被调用函数;
  4. (被调用函数执行前先保存调用者的上下文信息)什么信息保存到哪里,此处不详;
  5. 被调用函数获得保存局部变量的临时堆栈空间
  6. 被调用函数的(上下文信息)簿记寄存器被初始化。现在,被调用函数必须能够找到参数的值;
  7. 被调用函数主体执行;
  8. 返回值(如果有)将在堆栈被释放后放到一个安全的地方,函数调用者的(上下文信息)簿记寄存器和其它寄存器将被恢复,先前的返回地址被弹出堆栈,并返回原调用处。

一些机器的体系对函数的调用返回过程的处理的工作不甚重视,而看重其它方面的。重视与否将是以下内容的主题。

Dividing the Work 工作分解

函数的调用和返回的额外工作(也就是Prologs and Epilogs)应该由函数调用方还是被调用方来处理呢? 由上面可知,调用方的一些状态信息必须调用时保存起来,在返回时恢复。这些状态信息包括调用的返回地址、参数的地址信息(如堆栈指针)和一些寄存器的值(kemin:还是没有说是什么)。这些信息既可由调用方也可由被调用方来保存。如果是前者,那么它知道哪些寄存器的值是需要在返回时恢复的;从而,被调用者也就不必保存那些没用到的寄存器的值。

程序空间对工作分配起着重要的作用。如果状态信息由被调用者保存,那保存和恢复代码每函数一套;如果由调用者来保存,保存和恢复代码每调用一套。而函数至少会被调用一次,所以代码每函数一套将节省程序空间。如果保存和恢复代码量大,那么用一个特殊的调用子程序来专门实现将更加节省代码空间。同样的,抽离回返过程的通用代码也将缩小代码空间。如果保存和返回代码是真的通用代码,那么调用者的状态信息必须放在一个被调用者知道的地方。当然这种代码共享与抽离也有一定的开销,是折中不是极端。

Passing Arguments 传递参数

调用方负责参数传递,这些参数的所在位置也最终成为被调用方的环境。 参数可以以几种方式传递。其中之一是调用方创建一块包括参数的内存区,并将该内存的地址传给被调用方。这种方式需要用到额外的寄存器,因此增加了状态信息的量,降低了调用的效率。

调用方一般知道被调用方的堆栈帧的位置。调用方可以把参数存到这个堆栈帧的边上,这样被调用方可以很容易的找到它,而不像上面那样需要额外的开销。这种方式需要堆栈必须分配在连续的内存区域。

由于大多数的函数一般传少量的几个的参数,所以一个很吸引人的效率提升手段就是将这些参数放在寄存器。这种机制需要特殊的寄存器体系(?)。目标已经证实此方法可行。

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.