📜  编译器设计-运行时环境

📅  最后修改于: 2021-01-18 05:28:44             🧑  作者: Mango


程序作为源代码仅仅是文本(代码,语句等)的集合,并且要使其生动起来,就需要在目标计算机上执行操作。程序需要内存资源才能执行指令。程序包含过程名称,标识符等,它们需要在运行时与实际内存位置进行映射。

在运行时,我们是指正在执行的程序。运行时环境是目标计算机的状态,可以包括软件库,环境变量等,以向系统中运行的进程提供服务。

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

激活树

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

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

Temporaries Stores temporary and intermediate values of an expression.
Local Data Stores local data of the called procedure.
Machine Status Stores machine status such as Registers, Program Counter etc., before the procedure is called.
Control Link Stores the address of activation record of the caller procedure.
Access Link Stores the information of data which is outside the local scope.
Actual Parameters Stores actual parameters, i.e., parameters which are used to send input to the called procedure.
Return Value Stores return values.

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

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

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

. . .
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;
   }
. . . 

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

激活树

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

存储分配

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

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

  • 过程:它们的文本部分是静态的,但是以随机方式调用。因此,堆栈存储用于管理过程调用和激活。

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

静态分配

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

堆栈分配

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

堆分配

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

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

堆分配

如上图所示,代码的文本部分分配有固定的内存量。堆栈和堆内存的分配是分配给程序的总内存的最大值。两者都缩小并且彼此对抗。

参数传递

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

r值

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

左值

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

例如:

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

从此示例中,我们了解到常数值(例如1、7、12)和变量(例如日,周,月和年)都具有r值。只有变量具有l值,因为它们也代表分配给它们的内存位置。

例如:

7 = x + y;

是一个L值错误,因为常数7不代表任何存储位置。

形式参数

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

实际参数

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

例:

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

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

价值传递

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

通过参考

在按引用传递机制中,实际参数的l值被复制到被调用过程的激活记录中。这样,被调用的过程现在具有实际参数的地址(存储位置),而形式参数则指向相同的存储位置。因此,如果更改了形式参数所指向的值,则应该看到对实际参数的影响,因为它们也应指向相同的值。

通过复制还原传递

该参数传递机制的工作原理与“传递引用”相似,不同之处在于在调用过程结束时对实际参数进行更改。在调用函数时,实际参数的值将被复制到被调用过程的激活记录中。形式参数(如果被操纵)对实际参数没有实时影响(因为传递了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的l值将复制到实际参数y。即使在过程结束之前更改了y的值,也将x的l值复制到y的l值,使其行为类似于按引用调用。

通过名字传递

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