编译器设计 - 运行时环境

作为源代码的程序仅仅是文本(代码、语句等)的集合,要使其活跃,需要在目标机器上执行操作。程序需要内存资源来执行指令。程序包含过程名称、标识符等,这些名称需要在运行时与实际内存位置进行映射。

运行时是指正在执行的程序。运行时环境是目标机器的状态,可能包括软件库、环境变量等,为系统中运行的进程提供服务。

运行时支持系统是一个包,主要由可执行程序本身生成,并促进进程与运行时环境之间的进程通信。它负责在程序执行时分配和取消分配内存。

激活树

程序是组合成多个过程的一系列指令。过程中的指令按顺序执行。过程具有开始和结束分隔符,其中的所有内容称为过程主体。过程标识符和其中的有限指令序列构成过程主体。

过程的执行称为其激活。激活记录包含调用过程所需的所有必要信息。激活记录可能包含以下单元(取决于所使用的源语言)。

临时变量 存储表达式的临时值和中间值。
本地数据 存储被调用过程的本地数据。
机器状态 在调用过程之前存储机器状态,如寄存器、程序计数器等。
控制链接 存储调用者过程的激活记录的地址。
访问链接 存储本地范围之外的数据信息。
实际参数 存储实际参数,即用于将输入发送到被调用过程。
返回值 存储返回值。

每当执行一个过程时,它的激活记录都会存储在堆栈中,也称为控制堆栈。当一个过程调用另一个过程时,调用者的执行将暂停,直到被调用过程完成执行。此时,被调用过程的激活记录存储在堆栈中。

我们假设程序控制以顺序方式流动,当调用一个过程时,其控制权将转移到被调用过程。当执行被调用过程时,它将控制权返回给调用者。这种控制流类型使得以树的形式表示一系列激活变得更容易,称为激活树

为了理解这个概念,我们以一段代码为例:

. . .
printf(“Enter Your Name: “);
scanf(“%s”, username);
show_data(username);
printf(“Press any key to continue…”);
. . .
int show_data(char *user)
   {
   printf(“Your name is %s”, username);
   return 0;
   }
. . . 

下面是给定代码的激活树。

Activation Tree

现在我们明白了过程是以深度优先的方式执行的,因此堆栈分配是过程激活最合适的存储形式。

存储分配

运行时环境管理以下实体的运行时内存需求:

  • 代码:它被称为程序的文本部分,在运行时不会改变。它的内存需求在编译时是已知的。

  • 过程:它们的文本部分是静态的,但它们以随机方式调用。这就是为什么使用堆栈存储来管理过程调用和激活的原因。

  • 变量:变量仅在运行时才为人所知,除非它们是全局变量或常量。堆内存分配方案用于管理运行时变量的内存分配和取消分配。

静态分配

在此分配方案中,编译数据绑定到内存中的固定位置,并且在程序执行时不会改变。由于内存需求和存储位置是预先知道的,因此不需要用于内存分配和取消分配的运行时支持包。

堆栈分配

过程调用及其激活通过堆栈内存分配进行管理。它采用后进先出 (LIFO) 方法,这种分配策略对于递归过程调用非常有用。

堆分配

过程的本地变量仅在运行时分配和取消分配。堆分配用于动态地为变量分配内存,并在不再需要变量时将其收回。

除了静态分配的内存区域外,堆栈和堆内存都可以动态和意外地增长和缩小。因此,它们不能在系统中提供固定数量的内存。

堆分配

如上图所示,代码的文本部分分配了固定数量的内存。堆栈和堆内存排列在分配给程序的总内存的两端。两者相互收缩和增长。

参数传递

过程之间的通信媒介称为参数传递。调用过程的变量值通过某种机制传输到被调用过程。在继续之前,首先了解一些与程序中的值有关的基本术语。

r-value

表达式的值称为其 r 值。如果单个变量中包含的值出现在赋值运算符的右侧,则该值也将成为 r 值。r 值始终可以分配给其他变量。

l 值

存储表达式的内存位置(地址)称为该表达式的 l 值。它始终出现在赋值运算符的左侧。

例如:

day = 1;
week = day * 7;
month = 1;
year = month * 12;

从这个例子中,我们了解到 1、7、12 等常量值和 day、week、month 和 year 等变量都具有 r 值。只有变量才具有左值,因为它们还代表分配给它们的内存位置。

例如:

7 = x + y;

是左值错误,因为常数 7 不代表任何内存位置。

形式参数

获取调用者过程传递的信息的变量称为形式参数。这些变量在被调用函数的定义中声明。

实际参数

其值或地址被传递给被调用过程的变量称为实际参数。这些变量在函数调用中指定为参数。

示例:

fun_one()
{
   int actual_parameter = 10;
   call fun_two(int actual_parameter);
}
   fun_two(int formal_parameter)
{
   print formal_parameter;
}

形式参数保存实际参数的信息,具体取决于所使用的参数传递技术。它可能是一个值或一个地址。

按值传递

在按值传递机制中,调用过程传递实际参数的右值,编译器将其放入被调用过程的激活记录中。然后,形式参数保存调用过程传递的值。如果形式参数保存的值发生更改,则不会对实际参数产生影响。

按引用传递

在按引用传递机制中,实际参数的左值被复制到被调用过程的激活记录中。这样,被调用过程现在具有实际参数的地址(内存位置),并且形式参数引用相同的内存位置。因此,如果形式参数指向的值发生改变,则实际参数也应受到相应影响,因为它们也应指向相同的值。

通过复制-恢复传递

此参数传递机制与"通过引用传递"类似,不同之处在于实际参数的更改是在调用过程结束时进行的。在函数调用时,实际参数的值将复制到调用过程的活动记录中。如果操纵形式参数,则不会对实际参数产生实时影响(因为传递了 l 值),但当调用过程结束时,形式参数的 l 值将复制到实际参数的 l 值中。

示例:

int y; 
calling_procedure() 
{
   y = 10;     
   copy_restore(y); //l-value of y is passed
   printf y; //prints 99 
}
copy_restore(int x) 
{     
   x = 99; // y still has value 10 (unaffected)
   y = 0; // y is now 0 
}

当此函数结束时,形式参数 x 的左值被复制到实际参数 y。即使在过程结束之前更改了 y 的值,x 的左值也会被复制到 y 的左值,使其表现得像通过引用调用一样。

按名称传递

像 Algol 这样的语言提供了一种新的参数传递机制,其工作方式类似于 C 语言中的预处理器。在按名称传递机制中,被调用的过程的名称被其实际主体替换。按名称传递以文本方式将过程调用中的参数表达式替换为过程主体中的相应参数,以便它现在可以对实际参数起作用,就像按引用传递一样。