众所周知,在程序执行过程中,变量、常量、程序代码都需要占用一定的存储区域,有效地组织与分配存储区域是一个值得讨论的话题。通常,存储管理与具体目标机的体系结构是密切联系的,凭空讨论存储管理没有太大意义。在经典编译技术中,关于存储管理的理论并不太丰富,更多是凭借个人的经验与技巧完成的。因此,很多设计者更愿意将其视为一种管理艺术,而非科学技术。
本章涉及的存储管理是以讨论目标程序运行过程中的存储空间为重点,故也称为“运行时存储管理”。事实上,在编译器设计中,深入研究目标程序运行时的存储空间远比讨论编译器本身的存储空间更有意义。目标程序要实现一次运行,除了必要的可执行代码,还必须依托一个运行时环境。而许多与目标程序运行相关的工作都必须依赖这个环境,包括源程序中各种对象的存储分配及访问机制、参数传递、操作系统接口等。
首先引入几个重要的概念,这可能更有助于读者学习本章内容。
(1)逻辑地址与物理地址。在通用机平台上,编译器生成的目标程序通常不是独享整个系统的存储资源的,其享有的存储空间是由操作系统依据特定的算法分配的。对于物理存储资源调配情况,编译器是完全无能为力的。那么,目标程序如何实现对存储空间的访问呢?事实上,这个过程是由编译器与操作系统甚至目标机协作完成的。下面,笔者举一个汇编程序存储分配的例子,如图8-1所示。
从ASM文件到实际运行的过程可以分为以下两个阶段:
第一,汇编、链接阶段。ASM文件中并没有明确指出A、B符号的地址,只是说明了A、B符号所需的存储空间。经过汇编、链接之后,形成了可执行文件。一个可执行文件通常是以段的形式组织的,其中一部分用于存储数据(如变量、常量等),而另一部分用于存储可执行代码。在假设各段起始地址为0的情况下,汇编器会为各符号计算逻辑地址,即相对于段首地址的偏移。实际上,汇编、链接是两个不同的阶段,它们的工作职责也是完全不同的。囿于篇幅,本书不再深入阐述两者的差异。当然,并不排除用户在ASM中指定符号逻辑地址的可能性,在这种情况下,汇编器就只能按照用户的实际需要分配逻辑地址。注意,只是逻辑地址而己。
图8-1 汇编程序的存储分配示例(www.daowen.com)
第二,装入阶段。当一个可执行文件被执行时,操作系统会按照一定的调度顺序将可执行文件中的段读入物理存储空间中。通常,物理存储空间是共享的,完全是由操作系统管理与分配的。当某一数据段被装入到物理存储空间之后,操作系统会为各符号计算物理地址,即实际分配得到的存储地址,并修改相应的引用。至于操作系统如何确定引用点的问题并不难解决。通常,引用点的信息是由汇编器产生的,并写入可执行文件中。操作系统只需按照约定的格式读取。即可获得各引用点的信息。在操作系统中,这个过程被称为“重定位”。关于重定位更详细的内容属于操作系统讨论的范畴,这里就不再赘述。
(2)逻辑地址空间的组织形式。用户程序的逻辑地址空间的管理与组织是由编译器、操作系统、目标机共同完成的。这里,笔者介绍一种最普遍的模型,如图8-2所示。通常,编译器会将逻辑地址空间划分为五个区域,即代码区、静态区、堆区、栈区、空闲区,并将用户程序中的各种程序数据分类存储。
图8-2 逻辑地址空间模型
代码区主要用于存储程序代码。程序代码在程序运行过程中是保证不变的,对于拥有可写ROM的目标机结构来说,代码区一般是存放在ROM中。这样既保证了代码安全,也节省了有限的RAM空间。
静态区主要用于描述用户程序的静态数据,包括全局变量、静态变量等。通常,静态区允许用户程序通过地址直接进行寻址。
栈区、堆区主要用于解决程序运行过程中动态存储分配的问题。通常,用户程序的非静态局部变量都是有一定生存周期的,当某一函数执行完毕后,隶属于该函数的非静态局部变量的存储空间就会被释放。在程序设计语言中,这种动态分配、回收的例子并不少见,例如指针目标空间的分配、回收等。在静态区中,实现空间的动态分配、回收有相当的难度。然而,在栈式结构或堆式结构中实现这一功能是比较容易的,因此,设计者更愿意选择栈、堆结构,而非静态结构。那么,动态存储分配是否必须基于栈或堆结构实现呢?严格来说,的确如此。不过,栈、堆结构并不是所有目标机都支持的,有些目标机可能只支持代码区、静态区。那么,编译器就不得不在静态区中模拟某些动态分配、回收的过程,以满足相应的需求。即便如此,这种分配策略的本质仍然是静态的,是在编译过程中由编译器完成的。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。