过程:封装代码的方式

108 阅读6分钟

什么是程序计数器?

程序计数器(Program Counter)是计算机中的一个特殊寄存器,用于存储当前正在执行的指令的地址或位置。它帮助计算机按照程序的顺序执行指令,并在需要时进行跳转和分支。

每当计算机执行一条指令后,程序计数器会自动递增,将它的值更新为下一条指令的地址,以便计算机知道下一步要执行哪个指令。

什么是过程?

过程是软件中一种很重要的抽象,它提供一种封装代码的方式,用一组指定参数和一个可选的返回值实现了某种功能,然后,我们在程序的不同地方调用这个过程,通过【过程】作为抽象机制来隐藏某个行为的具体事项。

所以,对于不同的编程语言,过程的形式是多种多样:函数、方法、子例程、处理函数等等。殊途同归,只是叫法不一样。当前文章中的【过程】理解成函数即可。

本文假设过程 P 调用过程 Q,Q 执行后返回到 P,这一系列动作,都涉及到哪些流程?也就是:

本文假设函数 P 调用函数 Q,Q 执行后返回到 P,这一系列动作,都涉及到哪些流程?

传递控制: 在进入函数 Q 的时候,程序计数器【在寄存器中】必须被设置为 Q 代码的起始地址,然后在返回的时候,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。

传递数据: P 必须能够向 Q 提供一个或多个参数,Q 必须能够像 P 返回一个值。

分配和释放内存: 在开始时 Q 可能需要为局部变量分配空间, 而在返回前,又必须释放这些存储空间。

传递控制

当函数 P 调用函数 Q 时,会发生一种称为函数调用栈帧(Function Call Stack Frame)的操作。在函数调用栈帧中,用于保存函数执行过程中的临时数据、局部变量以及函数调用关系的信息。

当函数调用发生时,会将返回地址和参数值等信息压入栈中,然后分配一段空间用于存储局部变量。当函数执行完毕后,栈桢被释放,栈顶指针会回退到之前的位置,以便继续执行调用函数的指令。

image.png

我们来阐述下栈桢的变化:

  • P 的栈桢中包含了 P 的返回地址(返回地址1)和参数(参数1、参数2等)。
  • 当 P 调用 Q 时,将 Q 的返回地址压入 P 的栈中,指明当 Q 执行完毕后需要返回到 P 的下一条指令的位置。
  • P 将参数传递给 Q,将参数值压入 Q 的栈中,以便 Q 在执行时可以获取这些参数。
  • Q 的栈桢中包含了 Q 的返回地址(返回地址2)和参数(参数1、参数2等)。
  • Q 执行过程中,可以访问和操作自己的局部变量。
  • 当 Q 执行完毕后,Q 的栈桢被释放,栈顶指针回退到 Q 调用的位置,继续执行 P 的指令。
  • P 继续执行其余的操作,包括使用 Q 的返回值(如果有)和继续执行其他指令。

数据传递

当函数 P 调用函数 Q 时,P 的代码必须首先把参数复制到适当的寄存器中。类似的,当 Q 返回到 P 时,P 的代码可以访问寄存器中返回的值。

函数 P 调用函数 Q,有 n 个参数,且 n > 6,那么 P 的代码分配的栈桢必须要能容纳 7 到 n 号参数的存储空间。

在 x86-64 的架构中,寄存器的数量有限,每个寄存器可以容纳的数据量也有限,因此约定了最多可以通过寄存器传递 6 个整数参数,超过 6 个参数的部分通常存储在栈上。

前面 6 个参数通过寄存器传递,后面两个通过栈传递。要把参数 1 到 6 复制到对应的寄存器,把参数 7 到 n 放到栈上,而参数 7 位于栈顶。

内存管理

在大多数情况下,函数所需的本地存储区域可以完全放在寄存器中,因为寄存器具有更快的访问速度。然而,有些情况下,函数的本地数据可能会超出寄存器的容量限制,这时就需要将这些数据存放在内存中。

当函数的本地数据无法完全存放在寄存器中时,它们将被分配在内存中的栈上,这部分内存称为栈帧,其中包含了函数的局部变量和其他相关信息。

在调用过程中,有一些寄存器是被调用者负责保存的,而不是调用者。这意味着在调用之前,调用者要先将这些寄存器的值保存到一个安全的地方,然后调用被调用的函数,再在需要的时候取回这些值。

当一个函数 P 调用另一个函数 Q 时,Q 必须保存 P 使用的一些寄存器的值,以确保在 Q 返回到 P 时,这些寄存器的值保持一样。

Q 可以通过两种方式保存寄存器的值,要么根本不改变它们,要么将原始值压入栈中,然后在返回之前从栈中弹出旧值。

这些被保存的寄存器的值会在一个叫做栈桢的区域中创建,它是栈的一部分。有了这个约定,P 的代码可以安全地将值存储在被调用者保存的寄存器中,只需要在调用 Q 之前将之前的值保存到栈上,然后调用 Q,然后继续使用寄存器中的值,不必担心值被破坏。

在函数 P 两次调用 Q 时,第一次调用时必须保存 X 的值以备后续使用。类似地,在第二次调用中,也必须保存 Q(y) 的值。使用GCC生成的代码中,使用了两个被调用者保存的寄存器。

%rbp 寄存器保存了 X 的值; %rbx 寄存器保存了 Q(y) 的值;

在函数开始时,将这两个寄存器的值保存到栈中。在第一次调用 Q 之前,将参数 X 复制到 %rbp 中。在第二次调用 Q 之前,将此次调用的结果复制到 %rbx 中。

在函数的结尾,从栈中弹出这两个寄存器的值,恢复被调用者保存的寄存器值。注意,弹出的顺序与压入的顺序相反,遵循栈的后进先出规则。

栈的原则为我们提供了适当的策略,当函数被调用时分配局部存储空间,当返回时释放存储空间。


内容来源:《深入理解计算机系统》

如果您对本篇文章中提到的问题有任何疑问或想法,请在评论区留言,我将尽力回复。

微信公众号「小道研究」,获取更多关于前端技术的深入分析和实践经验。