运行时存储组织一节主要讨论在代码运行时的内存空间中如何存放所需的变量

本节中最复杂的问题来自于函数的嵌套定义,此时对外层嵌套函数中的变量寻址将会产生一些问题,例如

1
2
3
4
5
def func1():
x1 = 0
def func2():
print(x1)
func2()

此时再func2中如何定位定义在func1中的变量x1的地址?这就是本节需要解决的问题

运行时存储组织概论

运行时存储组织的几个主要问题:

  • 数据表示
  • 表达式计算
  • 存储分配策略
  • 过程实现(特别是过程的递归调用

数据表示

  • 对基本类型数据,使用已规定的数据大小存储
  • 对于数组/结构/对象等复合类型,使用一块连续的存储空间存储
  • 对象的成员函数/方法,存储在其所属类的代码区,每个类存放一份

表达式的计算

  • 在栈区计算:中间结果或运算数存放在活动记录或寄存器中
  • 在运算数栈计算:为表达式计算开辟专门的运算数栈

运行时的存储空间布局

  • 保留地址区
  • 代码区
  • 静态数据区
  • 共享库和分别编译模块区
  • 动态数据区(堆区、栈区)

存储分配策略

  • 静态分配:编译期分配
  • 动态分配
    • 栈式分配:拉栈,静态确定栈大小也属于动态分配
    • 堆式分配:堆分配和释放,例如C++中的new

活动记录

活动记录是相对于过程或函数而言的,记录此过程的局部变量实参临时中间值等数据和必要的控制信息

活动记录具体体现为运行栈上的栈帧


对于不支持过程嵌套定义的语言,活动记录的实现很简单,只要维护栈顶地址、栈帧中的返回地址即可

这是因为此类语言所有过程都定义在全局作用域,过程能够访问的过程外数据只有全局变量和参数

  • 全局变量可以直接根据地址寻址
  • 参数通过传参规范在寄存器中或栈上寻址

对于支持过程嵌套定义的语言,过程可以访问的过程外数据不止包括全局变量和参数,还包括外层过程的局部变量

这里的外层过程指的是定义此嵌套过程的过程,访问此类局部变量需要先找到外层过程的活动记录

而外层过程不一定总是直接调用嵌套过程,具体地,在下述代码中:

1
2
3
4
5
6
7
8
9
def p():
x1 = 0
def q():
def r():
print(x1)
r()
r()
q()
p()

过程记录栈的内容为p→q→r→r→r→……

此时过程r需要使用过程p中定义的x1,但过程r的直接调用者并不是过程p,且由于过程r可能经过任意轮递归,过程p在栈中的位置是不确定的

这是定义嵌套和调用嵌套的矛盾,此时需要使用数据结构来显式维护定义嵌套链调用嵌套链

静态链和动态链法

在活动记录中:

  • 使用静态链 (SL) 指向当前过程在定义嵌套链中的上一级,即定义者的最近一次调用
  • 使用动态链 (DL) 指向当前过程在调用嵌套链中的上一级,即调用者;技术上动态链总是指向栈中上一个活动记录
  • 每个活动记录分配固定位置的栈上地址,存储SL和DL

Display表法

Display表静态链的另一种实现,在Display表中,D[i]表示当前过程定义链中第i层的过程的活动记录;D[0]总是指向全局记录或主函数记录

在函数调用发生时,Display表可能发生表项替换,此时需要将换下的表项保存在栈上的活动记录中,当函数返回时用栈上保存的表项替换Display表中的原有表项

由于一个过程在定义链中的层数是编译期已知的,因此不需要进行Display表层数的动态维护

Display表的更新和保存见下图:

image-20250103183734412

这里,S调用P,P调用Q,Q调用R,R调用P;S、P并行定义,P定义Q,Q定义R

动态作用域和静态作用域

对于在过程调用中,对于变量引用的定值选择存在两种不同的模式:

  • 静态作用域(词法作用域): 最常见的模式,变量的引用按照静态链(定义链)中最近的活动记录中的值处理
  • 动态作用域:变量的引用按照动态链(调用链)中最近的活动记录中的值处理