函数调用机制剖析

摘要

本文从问题定义、问题拆解、问题分析、解决思路、实现方案这几个维度,相对比较完整地介绍函数调用技术实现机制。阅读完本文档,预期读者将对函数调用机制有一个比较全面的理解,并且会体会到全面考虑技术各个维度的重要性。

一、问题定义

函数调用的目标是:Fun A调用Fun B,当执行完Fun B后,可以从Fun B返回并继续执行Fun A的下一条指令

如何实现这个目标呢?

二、问题拆解及分析

如何实现函数调用需要达成的目标:Fun A调用Fun B,当执行完Fun B后,可以从Fun B返回并继续执行Fun A的下一条指令。

从整体上看,首先要选取适合完成该功能的数据结构,Fun A调用Fun B后又要能回到Fun A,我们很自然地会想到具备“先进后出”特征数据结构——栈,即采用栈数据结构来辅助完成函数调用功能。

从过程细节上看,如何解决“Fun A调用Fun B,当执行完Fun B后,可以从Fun B返回并继续执行Fun A的下一条指令”这一问题,可以进一步拆解为:

  • 参数如何传递
  • 执行控制权如何传递
  • 结果如何返回
  • 执行控制权如何返回移交
  • 被调用函数执行期间所需存储资源及释放

1、参数如何传递

问题1.1:需要传递参数的情况下,调用函数如何将 参数 传递给被调用函数?

分析:由这个问题,我们很容易联想到生活中的例子。为了保证买卖双方能够交接“货物”,双方需要约定特定地点来“交易”。

相应地,调用函数和被调用函数也要进行约定,调用函数将参数放到双方约定好的地方,被调用函数执行时去取参数即可。

问题1.2:可以放在哪些地方呢?

分析:计算机中存放数据的地方有两个,一个是内存,一个寄存器(不考虑磁盘)。

在函数调用过程中,我们可以约定特定栈内存地址,或者 特定寄存器,用来存放各个入参。

与此同时,为了避免存取值混淆,还需要约定各个入参与对于栈内存、寄存器之间的一一对应关系。

问题1.3:毕竟寄存器数量非常有限,如果采用寄存器传递,万一入参数量超过可用寄存器数量,怎么办?

分析:较于内存访问,寄存器访问速度更快,所以能使用寄存器时就尽可能使用。

万一寄存器数量不够,很自然想到可以借助于栈内存来存放传递剩余的入参。

问题1.4:为什么要把值放在特定的地方呢?我直接告诉被调用函数去我实际存储数据地方去取不就行了?

分析:如果告诉被调用函数去实际存储地方取值,此时入参是内存地址,而不是内存地址处存储的值(假设存储的值不是指针值)。

这样做的确有很多好处:

1、数据源唯一,保证数据一致性

此时指向原始内存地址,所以对数据的任何访问修改,都是直接反映在原始数据上;如果入参传递的是数据,这时数据也就有了两份,一份是原始数据,一份是拷贝数据,后续调用函数对拷贝数据的任何访问修改,由于没有数据同步功能,也就不会最终反映到原始数据上,为预期的数据一致性埋下了失败的可能。

2、入参拷贝工作量大大减少

对于连续型数据,比如数组,这样做可能省却了拷贝大量数组元素的繁冗过程。

问题1.5:直接传地址参数这种方式,有缺点吗?

分析:正所谓在某种程度上,优点便是缺点,这样做可能会导致不应该的原始数据被错误修改等问题。其实函数调用都是使用传值方式,只不过有的值是地址值罢了。

2、执行控制权如何传递

问题2.1:调用函数如何将 执行控制权 移交给 被调用函数?

分析:程序指令执行,是通过PC指针来控制实现的,PC是CPU执行指令的基础。PC指向哪里,CPU就会执行哪里的指令。执行控制权移交给被调用函数,等价于 将PC指针指向被调用函数入口地址即可。

问题2.2:谁来做“将PC指针指向被调用函数入口地址”这件事?

分析:毫无疑问,必须由调用函数来做,此时被调用函数还没执行呢!

3、结果如何返回

问题3.1:需要结果返回的情况下,被调用函数如何将 结果 返回给调用函数?

分析:这也属于数据传递范畴,可以借鉴前述参数传递机制,将返回结果放在双方约定好的地方。比如特定栈内存地址 或者 特定寄存器中。

问题3.2:若返回结果有很多个值,怎么办?

分析:这个问题和上述入参传值寄存器不够用问题类似,解决方法也类似,可以通过栈内存来弥补不足。

4、执行控制权如何返回移交

问题4.1:被调用函数执行结束后,如何将 执行控制权 返回移交给调用函数,以让调用函数继续执行下去?

分析:执行控制权不仅能放出去,还得能收回,否则谁还敢进行函数调用呢!

其实这也是执行控制权传递范畴,只是由被调用函数——>调用函数,而刚开始进行函数调用时,则是由调用函数——>被调用函数。

调用函数——>被调用函数时,调用函数将PC指针设置为被调用函数入口地址即可。很明显,被调用函数——>调用函数时,也应该是由被调用函数重新设置PC,但是此时PC指针应该指向调用函数指令序列中哪个指令地址呢?

因为被调用函数不知道调用函数接下来会执行哪一条指令,而这个信息当且仅当只有调用函数自己才知道。

为了能够协助被调用函数执行结束时,能够顺利将PC指针指向调用函数合适的指令地址,以使得调用函数在函数调用完成后能够继续执行指令,需要调用函数提供下一条继续执行指令的地址,即返回地址。

为了使得被调用函数能够获取到返回地址,同样需要将返回地址放在特定的地方,以方便被调用函数执行结束时,能够取出返回地址并且将PC指针指向返回地址,从而使得执行控制权能够及时返回移交,并且调用函数能够继续执行后续的指令系列。

关于返回地址,我们还有如下一系列需要考虑的问题

问题4.2:who---谁提供返回地址

分析:毫无疑问,因为只有调用函数自己知道接下来执行的指令,所以只能是调用函数提供。

问题4.3:what---返回地址是什么

分析:提供返回地址的目的,是能够使得从当前被调用函数返回,调用函数继能够续执行下去,那么返回地址也就是函数调用的下一条指令地址。

问题4.4:when---什么时候提供返回地址

分析:很明显,进行函数调用,执行控制权移交前,需要提供返回地址,其他时机都不合适。

问题4.5:where---返回地址放在哪里?

分析:本质上返回地址也是数据,和前面讨论的数据传递类似,需要放在双方约定好的地方,这样才能保证被调用函数结束时,能够获取到返回地址,并及时将PC指针指向返回地址。

5、存储资源如何使用及释放

问题5.1:被调用函数执行期间需要哪些存储资源?

分析:为方便数据临时存储及更高效处理数据等,被调用函数执行期间需要栈内存和寄存器资源。

问题5.2:这些存储资源被用来存储哪些内容?

分析:

A:内存资源管理

一般情况下,通过寄存器可以存储局部变量数据,但是可能存在寄存器资源不够或者必须使用内存资源(比如对局部变量取地址操作&必须产生地址)情况, 相应地函数栈帧中需要有专门的局部变量存储区域等。

B:寄存器资源管理

因为寄存器资源有限且为所有过程共享,所以需要确保被调用函数不能覆盖调用函数后续需要继续使用的寄存器值,需要有相应的规则来说明哪些寄存器值需要调用者保存,哪些值需要被调用者保存。确定要保存的寄存器后,还需要约定相应寄存器值保存在哪?后续如何恢复?等问题。

问题5.3:寄存器资源有限且被多个过程共享使用,如何协调公用寄存器资源使用?

分析:需要有相应的规则来保证。哪些寄存器在使用前,应该先被保存信息,然后才能被使用,否则使用完后寄存器原始值无法恢复,调用函数也就无法继续执行下去。

问题5.4:存储资源什么时机释放?

分析:由于存储资源有限,所以必须遵循有用有还原则,这样才能再借不难。当存储资源不再被使用时,便是被释放的时机。

什么时候存储资源不再被使用?

很明显,当函数执行结束,先前所有存储资源便不会再使用,此时需要释放所有存储资源。

三、问题解决

0、函数栈帧结构

From Intel® 64 and IA-32 Architectures Software Developer’s Manual

 

 

1、参数如何传递

在X86-64中,可以通过寄存器最多传递6个整型(即整数和指针)参数,并且寄存器使用有特殊约定顺序。寄存器使用的名字取决于要传递数据类型的大小。

如下图所示:

64位情况下,第一个至第六个参数分别使用%rdi、%rsi、%rdx、%rcx、%r8和%r9;

32位情况下,第一个至第六个参数分别使用%edi、%esi、%edx、%ecx、%r8d和%r9d;

如果参数大于6个,超过6个的部分就需要通过栈来传递。假设过程P调用过程Q,有n个整型参数,其中n>6,那么P分配的栈帧就必须能够容纳7~n参数。

下面是Intel关于函数传参机制详细说明

From Intel® 64 and IA-32 Architectures Software Developer Manuals

疑问1:为什么只提供6个传参寄存器,而不是更多呢?

分析1:寄存器资源比较稀缺,<=6个参数的函数基本覆盖了绝大部分函数场景,从性价比方面考虑较优。

疑问2:如果入参是浮点型数,该怎么传递呢?

分析2:针对浮点型参数,有专门浮点型寄存器,如下图示。从中可以看出支持最多传递8个参数,使用寄存器%xmm0来返回浮点值。

疑问3:如果函数入参既有整型也有浮点型,又该怎么传递呢?

分析3:因为整型与浮点型拥有各自寄存器资源,所以传递参数时各自使用自己对应寄存器,即指针和整数通过前述通用寄存器传递,而浮点值通过浮点寄存器传递。

疑问4:IA-32有哪些寄存器?各个寄存器的作用是什么?

分析4:

 

2、结果如何返回

在X86-64中,分别通过%rax和%ymm0寄存器来返回整型和浮点型值。

3、执行控制权如何返回移交

将控制权从函数P转移到函数Q时,只需要简单地将程序计数器PC设置为Q的代码起始位置,当从Q返回时,处理器必须记录好它需要继续执行P的代码位置。

Call指令:

在X86-64机器中,这个信息是通过指令call Q调用过程Q来记录的,该指令完成下述功能:

(1)把返回地址A压入栈中

(2)将PC设置为Q的起始地址

Ret指令:

该指令完成下述功能:

(1)从栈中弹出地址A

(2)把PC设置为A

最终使得完成函数调用后可以继续执行后续指令。

 From Intel® 64 and IA-32 Architectures Software Developer Manuals

4、存储资源如何使用及释放

必要情况下,被调用函数Q需要保存当前寄存器值,并在执行结束前进行恢复,此外还需要为本地变量分配栈内存及为函数调用构造入参函数区。函调调用结束后,通过偏移栈指针来实现对应开辟的栈帧会销毁。

在X86-64中,寄存器分为调用者保存寄存器和被调用者保存寄存器。

调用者保存寄存器:

调用者保存寄存器是调用者必须先将寄存器中的数据进行保存备份,被调用者可以随意使用并且不负责寄存器数据恢复。

被调用者保存寄存器:

被调用者保存寄存器,则需要被调用者在使用寄存器前,需要将当前寄存器值保存至栈中,之后才能使用寄存器,使用完成后还需将栈中存储的寄存器值恢复值寄存器中,以方便后续调用函数能够顺利执行。

疑问5:为什么要分两组,即调用者和被调用者,来保存公用寄存器资源,约定其中一方来保存寄存器资源,不是更好吗?

分析5:因为寄存器硬件资源为所有过程复用,所以为了避免 函数调用后寄存器资源值被破坏而使得调用函数无法继续执行问题,需要保存公用寄存器资源。
如何保存公用寄存器资源,需要先了解一下有通用寄存器使用情况:
一类是用来存放临时信息的寄存器,通常也被称为volatile信息。

一类是用来存放long-lived信息值,通常也被称为non-volatile信息。
真正需要保存的是第二类寄存器值。
具体到如何保存寄存器资源,可能有如下三种方法:
方案a:函数调用前,将所有被调用函数需要使用到的寄存器资源进行保存
方案b:在函数调用过程中,将所有用到的寄存器资源值解析保存,并在函数结束时恢复
方案c:约定调用者和被调用者,各自保存各自需要保存的寄存器资源值

接下来分别对这三种方案进行讨论:


如果采用方案a,调研者需要必须要保存所有non-volatile类型寄存器值。保存寄存器资源值,是通过将寄存器值保存到栈中,最终再将栈中数据恢复至寄存器。这里就涉及到栈 内存访问,我们知道,较于寄存器访问,内存访问速度要慢很多。如果被调用者只使用到极少寄存器,而需要调用者将所有寄存器都保存下来,明显会增加多次内存访问开销,性能会明显下降。

如果采用方案b,由被调用保存所使用到的non-volatile类型寄存器值,如果不需要使用,则不需要保存,很明显比方案a要好很多;
万一调研者后续需要使用某些volatile类型寄存器值,怎么办?

此时方案c,则能够较好地解决这个问题,调研者保存后续可能使用到的volatile类型寄存器值,而由被调用者保存使用到的non-volatile类型寄存器值,这样可以最大程度上保证性能最优。

四、典型范例

以《深入理解计算机系统》书中一个例子来说明函数调用汇编实现,旁边英文注释非常清晰,就不再做解释,主要提下面三点:

  1. 入参&arg1,&arg2分别通过%rdi、%rsi寄存器传递;
  2. 开辟栈帧,销毁栈帧通过subq和addq汇编指令来减少、增加栈指针来完成;
  3. Call和ret汇编指令配合使用;

五、小结

本文主要从问题定义、问题拆解、问题分析、解决思路、实现方案这几个维度,相对比较完整地介绍函数调用实现技术机制。限于篇幅原因,还有很多话题未进行深入讨论,比如函数调用机制采用栈技术的来龙去脉、函数入参顺序约定原因等等。从中也可以看出平常看似简单的函数调用,背后涉及很多内容,当且仅当将这些内容都搞清楚,才能对函数调用机制原理有非常清晰的理解。

六、参考资料

1、<<深入理解计算机系统>>

2、<< Intel® 64 and IA-32 Architectures Software Developer Manuals >>

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值