编译器设计-RunTime运行时环境
编译器设计-RunTime运行时环境
Compiler Design - Run-Time Environment
作为源代码的程序仅仅是文本(代码、语句等)的集合,要使其活动,它需要在目标计算机上执行操作。程序需要内存资源来执行指令。程序包含程序名、标识符等,运行时需要与实际内存位置进行映射。
所谓运行时,我们指的是正在执行的程序。运行时环境是目标机器的一种状态,它可以包括软件库、环境变量等,为系统中运行的进程提供服务。
运行时支持系统是一个包,主要由可执行程序本身生成,有助于进程与运行时环境之间的进程通信。它在执行程序时负责内存分配和取消分配。
激活树Activation Trees
程序是一系列指令组合成若干过程的序列。过程中的指令是按顺序执行的。过程有一个开始和结束分隔符,其中的所有内容都称为过程的主体。过程标识符及其内部的有限指令序列构成了过程的主体。
过程的执行称为其激活。激活记录包含调用过程所需的所有必要信息。激活记录可以包含以下单元(取决于使用的源语言)。
每当执行一个过程时,它的激活记录都存储在堆栈上,也称为控制堆栈。当一个过程调用另一个过程时,调用方的执行将被挂起,直到被调用的过程完成执行为止。此时,被调用过程的激活记录存储在堆栈上。
我们假设程序控制按顺序执行,当一个过程被调用时,它的控制权被转移到被调用的过程。当被调用的过程被执行时,它将控件返回给调用方。这种类型的控制流使以树(称为激活树)的形式表示一系列激活变得更容易。
为了理解这个概念,我们以一段代码为例:
. . .
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;
}
. . .
下面是给定代码的激活树。
现在我们知道过程是以深度优先的方式执行的,因此堆栈分配是过程激活的最佳存储形式。
存储分配Storage Allocation
运行时环境管理以下实体的运行时内存需求:
代码:它被称为程序的文本部分,在运行时不会更改。它的内存需求在编译时是已知的。
过程:它们的文本部分是静态的,但它们是以随机的方式调用的。这就是为什么堆栈存储用于管理过程调用和激活。
变量:变量只有在运行时才知道,除非它们是全局变量或常量。堆内存分配方案用于管理运行时变量的内存分配和取消分配。
静态分配Static Allocation
在这种分配方案中,编译数据被绑定到内存中的一个固定位置,并且在程序执行时不会改变。由于预先知道内存需求和存储位置,因此不需要内存分配和取消分配的运行时支持包。
堆栈分配Stack Allocation
过程调用及其激活通过堆栈内存分配进行管理。它适用于后进先出(LIFO)方法,这种分配策略对于递归过程调用非常有用。
Heap Allocation
仅在运行时才分配和取消分配过程本地的变量。堆分配用于动态地将内存分配给变量,并在不再需要变量时将其收回。
除了静态分配的内存区域外,堆栈和堆内存都可以动态和意外地增长和收缩。因此,不能在系统中为它们提供固定数量的内存。
如上图所示,代码的文本部分被分配了固定数量的内存。堆栈和堆内存被安排在分配给程序的总内存的最末端。两个人都在互相残杀和成长。
参数传递Parameter Passing
过程之间的通信媒介称为参数传递。调用过程中的变量值通过某种机制传递给被调用过程。在继续之前,先复习一些与程序中的值有关的基本术语。
右值
表达式的值称为其r值。如果包含在单个变量中的值出现在赋值运算符的右侧,则该值也将变为r值。r值总是可以分配给其他变量。
l值
存储表达式的内存(地址)的位置称为该表达式的l值。它总是出现在赋值运算符的左侧。
For example:
day = 1;
week = day * 7;
month = 1;
year = month * 12;
从这个例子中,我们了解到常数值(如1、7、12)和变量(如日、周、月和年)都有r值。只有变量才有l值,因为它们也表示分配给它们的内存位置。
For example:
7 = x + y;
是一个l值错误,因为常量7不代表任何内存位置。
形式参数Formal Parameters
接受调用方过程传递的信息的变量称为形式参数。这些变量在被调用函数的定义中声明。
实际参数Actual Parameters
将其值或地址传递给被调用过程的变量称为实际参数。这些变量在函数调用中指定为参数。
Example:
fun_one()
{
int actual_parameter = 10;
call fun_two(int actual_parameter);
}
fun_two(int formal_parameter)
{
print formal_parameter;
}
形式参数保存实际参数的信息,这取决于所使用的参数传递技术。它可以是一个值或地址。
传递值Pass by Value
在传递值机制中,调用过程传递实际参数的r值,编译器将其放入被调用过程的激活记录中。然后,形式参数保存调用过程传递的值。如果形式参数所保留的值发生了更改,则不会对实际参数产生影响。
通过引用传递Pass by Reference
在按引用传递机制中,实际参数的l值被复制到被调用过程的激活记录中。这样,被调用的过程现在拥有实际参数的地址(内存位置),而形式参数引用相同的内存位置。因此,如果更改了形式参数所指向的值,则应该看到对实际参数的影响,因为它们也应该指向相同的值。
按副本传递还原Pass by Copy-restore
此参数传递机制的工作原理与“按引用传递”类似,只是在调用过程结束时对实际参数进行更改。在函数调用时,实际参数的值被复制到被调用过程的激活记录中。如果操作了形式参数,则对实际参数没有实时影响(因为传递了l值),但是当调用过程结束时,形式参数的l值将复制到实际参数的l值。
例子:
Example:
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值,使其行为类似于通过引用调用。
按名称传递Pass by Name
像Algol这样的语言提供了一种新的参数传递机制,其工作原理类似于C语言中的预处理器。在按名称传递机制中,被调用的过程的名称将被其实际主体替换。Pass by name以文本方式将过程调用中的参数表达式替换为过程主体中的相应参数,以便它现在可以处理实际参数,这与Pass by reference非常类似。