函数的调用和返回

函数调用分析

对有递归特性的编程语言来说,区分函数定义和函数调用是十分有必要的。函数定义规定了函数的行为,函数每次调用都创建一个函数实例。虽然一个函数只有一个定义,随着时间的流逝,它可能产生很多不同的实例。对于一个递归函数来说,若干个实例可能会同时存在。

每个函数实例都需要分配内存空间,一个函数从调用到返回,它的活动记录包括以下内容:

  • 本地存储   包括参数、局部变量、以及编译器在实现函数时使用的辅助空间。
  • 返回信息   当这次调用结束时,执行流程应该在哪里继续?怎样继续?

活动记录指的是一个函数实例被分配的内存块。活动记录只存在于函数执行时,一旦函数返回,这块内存就会被释放。

活动记录

一个活动记录保存和一个实例(一次调用、一个函数)相关的状态。活动记录的主要任务是保存参数和局部变量。实际上,活动记录也可能会保存辅助信息。

编译器用函数声明生成活动记录。正如C语言中,一个活动记录就是一个集合,集合的每一项包括名字和类型(比如int a就是一项)。这些项存储在同一块内存中。下面的表达式把两个整数视为参数:

void Simple(int a, int b)  // 两个参数
{
    int temp1, temp2;  // 局部变量
    ....
}

从上面的声明可以看出,编译器生成活动记录时需要四个整数:a, b ,temp1和temp2。假设一个整数占据4个字节,那么这个活动记录至少需要16个字节。编译器不会在函数声明阶段产生一个活动记录,函数调用时,编译器会给这个函数实例分配内存空间。

栈是一种分配、回收函数实例的完美结构。函数调用时,它的活动记录会入栈并且初始化。活动记录必须在函数执行之前保持不变。函数退出时,该活动记录占据的内存空间会被释放。当前活动记录出栈,上一个函数的实例成为新的栈顶。因为函数调用随处可见,栈分配、回收的效率就十分重要了。

void A(void){
    B();
}

void B(void){
    C();
}

void C(void){
    printf("Hi!");
}

函数调用的时候有一个限制:函数必须以调用顺序的逆序返回。如果A先调用B,然后B调用C,最终函数必须以C、B、A的顺序返回。对于函数B的实例来说,在A实例之后和C实例之前返回是不可能的。只有正在执行的函数实例有权调用变量,其他所有实例都被暂时挂起。在上面的例子中,当C实例打印出“Hi!”的时候,A和B就被挂起。B在等待C退出,同样A也在等待B。

栈这种结构非常适合存储活动记录--它准备好当前实例,同时保持其他的实例挂起。当维护一个集合时,栈是一种相当常用的抽象数据结构,就像堆成一列的盘子。如果从顶部观察的话,只能看见最上面的盘子。栈定义了两种操作--入栈和出栈。入栈就是拿一个新盘子放在栈的顶部。新盘子阻挡你看到下面的盘子。出栈就是拿掉最顶部的盘子,下面那个盘子成为这个栈的新“栈顶”。

在这个函数调用的例子中,这堆盘子实际上就是活动记录。最上面的盘子就是当前正在执行函数的活动记录。当函数被调用之后,它的活动记录会出栈,下面的实例成为当前执行的函数。当该函数也退出后,它下面的实例继续执行。

活动协议

编译器生成代码时必须遵循一连串协议,为的是把控制流和信息从当前执行环境传递到被调用函数。不同的语言,甚至相同语言的不同编译器可能使用着略微不同的协议。

下面是一个用来传递参数、控制函数执行的简单协议:

  1. 当前执行环境分配一块内存,入栈同时生成了一个活动记录
  2. 初始化活动记录中的参数部分(这里是把当前执行环境的值复制到被调函数活动记录中)。
  3. 当前执行环境保存状态和返回信息。在参数之上入栈额外的4个字节。
  4. 被调函数成为新的当前执行环境,被调函数把它的局部变量入栈,执行函数。
  5. 被调函数正常执行,期间从它的活动记录中调用变量和参数。
  6. 当被调函数退出后,活动记录的部分内容出栈。
  7. 初始函数再度成为当前执行环境,依据返回信息继续执行,同时移除栈中被调函数的参数,被调函数的活动记录随之完全出栈。

注意到在这个协议中,局部变量并没有被初始化--栈指针仅仅给它们分配了地址空间。参数看起来像是有初始化了的局部变量。

由于栈是很多计算机活动的核心,大部分计算机操纵栈的指令都很特殊,为的是使上述协议高效实现。有些编译器使用寄存器代替栈来传递参数,现在我们忽略这种情况。为了让操作更快,有几个寄存器专门用于内务管理。SP寄存器用于保存栈顶地址。PC寄存器专用于保存当前执行指令的地址,RV寄存器专用于传递函数的返回值。其他的寄存器(R1,R2,等)是多功能的,用途广泛。

协议也规定出活动记录的空间是如何分配的。我们选一种简单的规则来进行说明。我们约定,把参数按从右到左的顺序入栈,(从右向左入栈和C编译器有关,这点会在后面详述),紧随其后的是返回地址,然后把局部变量按从上向下的顺序入栈。我们假设函数的返回值不会放在栈里,而是放在那个特殊的寄存器RV中。注意,这点决定了返回值不能超过一个机器字长,通常,这种情况会使用一种复杂的(常常也是颇费开销的)手段来处理,这里不提。就像大部分现代体系结构一样,实例栈也是从上往下增长,从高地址到低地址。下面以本文前段声明的Simple函数为例,给出一种活动记录布局:

协议中申明了参数和局部变量的分布,我们可以把活动记录视做一对相似且相邻的结构,一块放变量、另一块放参数,把返回地址夹在中间。SP刚好指向局部变量块的基地址,上面紧跟着返回地址,再上面是参数块。

struct SimpleActivationLocal{
    int temp2; // 偏移量是 0(从栈顶开始计算)
    int temp1; // 偏移量是 4
};

struct SimpleActivationParameters{
    int a; // 偏移量是12(从栈顶开始计算)
    int b; // 偏移量是16
}

在编译后的代码中,函数访问变量和参数时,靠的是它们各自距离栈顶的偏移量。在Simple函数里,参数b距离栈顶的偏移量是16字节。以下几点应当注意:

  • 协议允许编译器在不知道函数确切结束位置的情况下生成代码。
  • 每个函数实例运行同一份代码。
  • 当前使用的标识符不会在代码中留下任何痕迹,特殊变量的类型信息不会放在代码中,但是编译器能使用那些数据。

调用和返回

为了支持函数调用操作,我们现在往指令集里添加两条指令。这两条特殊的指令直接控制函数之间的执行流、保存和恢复执行状态。

Call指令比Jmp指令小一些,它能立刻把控制流重定向到另一个地址,它也能在栈上开辟空间、保存返回地址--当控制流返回到调用函数时,要执行的下一条指令的地址。我们列出函数名,以此控制它们的跳转。这实际上就是编译器的工作方法。链接器负责把函数名转换成地址。地址是运行时需要的东西,因为在那时我们不知道怎样在内存中通过函数名来找到一个函数。

# 跳转到strlen & 在栈中保存下一条指令的地址
Call <strlen>

Ret指令通常是函数体生成代码中的最后一句。当函数执行完毕时,释放占用的栈空间,把返回值保存在RV寄存器,函数会使用Ret指令来恢复到调用函数的上下文中。当Ret指令被调用时,我们假设SP寄存器已经指向栈上保存返回地址的区域,所以它能很容易的把这个返回地址放入PC寄存器。它又把栈上的这个返回地址出栈(此时SP寄存器指向最右边的参数)。此时PC寄存器正好指向之前调用函数的地方,就好像这个函数从来没有被调用过一样。

原文地址:https://www.cnblogs.com/xihui/p/9083915.html

时间: 2024-10-31 16:38:18

函数的调用和返回的相关文章

深入剖析php执行原理(4):函数的调用

本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的. 我们前面的章节弄明白了函数体会被编译生成哪些zend_op指令,本章会研究函数调用语句会生成哪些zend_op指,等后面的章节再根据这些op指令,来剖析php运行时的细节. 源码依然取自php5.3.29. 函数调用 回顾之前用的php代码示例: <?php function foo($arg1) { print($arg1); } $bar = 'hello php'; foo($bar); 在函数编译一章里已经分析过,

javascript学习笔记(二):定义函数、调用函数、参数、返回值、局部和全局变量

定义函数.调用函数.参数.返回值 关键字function定义函数,格式如下: function 函数名(){ 函数体 } 调用函数.参数.返回值的规则和c语言规则类似. 1 <!DOCTYPE html> 2 <html> 3 <head lang="en"> 4 <meta chaset="UTF-8"> 5 <title></title> 6 </head> 7 <body

调用带返回值得函数

函数调用语法: 函数名(实参列表)→ 函数调用时一个表达式→返回类型与函数声明的返回类型一致 函数声明:// 将两个数相加的结果返回static int Add(int a , int b){ return a + b:}函数调用:Add(3 , 5) // 该表达式返回int类型 int a = Add(3 , 5): // 将3和5相加的结果,保存到变量a 中 Console.WriteLine(Add(3 , 5 ));//将3和5相加的结果输出 函数声明://判断一个数是不是奇数stat

HDOJ 排序(pow函数的调用值及返回值类型)

注:pow函数包含在头文件math.h中,pow(a,b)既表示a的b次幂.pow函数的调用值与返回值都为浮点型, 既double N=pow(double a,double b)或float N(float a,float b). 例题: 排序 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 36580    Accepted Sub

初识函数、函数的参数、函数的参数传值、函数的调用形式、函数的返回值

初识函数 内置函数自定义函数 定义无参函数 #只打印执行语句的时候def foo(): print('from the foo')# print(foo.__doc__) #查看函数的文档注释 定义有参函数 #依赖外部传来的值的时候def bar(x,y): print(x) print(y) 定义空函数 #程序开发阶段经常使用,写程序框架的时候def auth(): pass 函数的参数 函数的参数介绍形参和实参的概念 def foo(x,y): #在函数定义阶段,括号内定义的参数->形式参数

逆向知识十一讲,识别函数的调用约定,函数参数,函数返回值.

逆向知识十一讲,识别函数的调用约定,函数参数,函数返回值. 在反汇编中,我们常常的会看到各种的函数调用,或者通过逆向的手段,单独的使用这个函数,那么此时,我们就需要认识一下怎么识别函数了. 一丶识别__cdecl 函数(俗称C Call),函数参数,函数返回值 首先写一个C Call的函数 1.返回值 int类型, 参数int 类型 高级代码: int __cdecl MyAdd(int a,int b) { return a + b; } int main(int argc, char* ar

函数的返回值、函数的调用、函数的参数

1.函数的返回值 ''' 1.什么是返回值 返回值是一个函数的处理结果, 2.为什么要有返回值 如果我们需要在程序中拿到函数的处理结果做进一步的处理,则需要函数必须有返回值 3.函数的返回值的应用 函数的返回值用return去定义 格式为: return 值 --------(值可以是是以数据类型) 注意: 1.return是一个函数结束的标志,函数内可以有多个return, 但只要执行一次,整个函数就会结束运行------即函数下面有再多代码也不会被执行 2.return 的返回值无类型限制,

JS中函数的本质,定义、调用,以及函数的参数和返回值

要用面向对象的方式去编程,而不要用面向过程的方式去编程 对象是各种类型的数据的集合,可以是数字.字符串.数组.函数.对象…… 对象中的内容以键值对方式进行存储 对象要赋值给一个变量 var cat={ "name":"喵1", "age":4, "family":["喵爸","喵妈"], "speak":function(){ console.log("喵喵

MFC浅析(7) CWnd类虚函数的调用时机、缺省实现

CWnd类虚函数的调用时机.缺省实现 FMD(http://www.fmdstudio.net) 1. Create 2. PreCreateWindow 3. PreSubclassWindow 4. PreTranslateMessage 5. WindowProc 6. OnCommand 7. OnNotify 8. OnChildNotify 9. DefWindowProc 10. DestroyWindow 11. PostNcDestroy CWnd作为MFC中最基本的与窗口打交