需要解决两个问题:
- 运行时空间是怎么管理的
- 存储空间是如何访问的,即如何进行地址映射(前面给出的变量的地址都是抽象地址,如何把抽象地址映射到物理地址就是要解决的问题)
一、目标程序运行时内存的划分
- 库代码区(library space):用于存放标准库函数的目标代码,pascal语言中用到的sin、cos等等。高级函数,如C语言中通过#include包含进来的库代码;【实际上这些函数都有一个代码函数来实现对应的这些功能,在用户写的程序中假如调用这些函数,必须得把这些函数的代码和程序中的代码进行连接装配组成一个完整的程序才可以,所以凡是在用户程序中调用的标准函数的代码可以称作是库函数代码,有的高级程序设计语言中还提供一些高级函数,可以把它们也链接装配进去。】
- 目标代码区(code space):用以存放编译生成的目标程序。实际上一个目标程序变成可执行文件需要连接-装配的过程,连接装配之后存放在目标代码区。【连接——将库函数等与程序建立对应的联系;装配——在一个要运行的程序中,实际上有很多子程序和程序部分构成,可以分块编译,分块编译后的程序要装配成一个完整的文件才可以;通常使用的运行是一个批命令(集成了连接编译运行)】
- 静态区(static space) :有些数据对象所占用的空间也能在编译时确定,其地址可以编译进目标代码中,这些数据对象通常存放在静态区中,如静态变量和全程变量
- 栈区(stack space):存放过程活动记录,该存储区被称作一个栈,一个元素是一个过程活动记录,调用函数时压栈,函数结束时退栈【每个函数调用的时候都有一个活动记录,比方说主程序调用的时候有一个,占用一块区域,主程序调用f1函数,就给f1分一个区,f1调f2又分一个区,每个区域的大小就是过程活动记录的大小。这个时候大家看就跟一个栈一样,一个活动记录作为栈的一个元素,调用一个函数就往栈里压入一个元素,等到函数结束的时候就相当于退栈。】
- 堆区(heap space):堆不是一个连续分配的模式,可以进行动态分配的空间管理,主要用于存放动态申请的数据对象 (如C,pascal ,Lisp等语言的malloc,calloc,free,new,delete)
- 栈区和堆区之间没有事先划好的界线,当目标代码运行时,栈区指针和堆区指针不断地变化,并朝着对方方向不断增长。如果这两个区相交,则表示出现了内存溢出
二、语言影响空间分配策略
1. 语言中是否允许函数、过程的递归调用
语言如果允许递归调用,则函数的形参、局部量可能对应是一串存储单元,不能采用静态的存储分配方式;如语言不允许有递归出现,则一个函数最多只能分配一个活动记录大小的空间。
比如阶乘函数f(n)
的如果用递归调用的时候给n分配一个空间,但是这个函数要递归调用到1,所以需要n个存储空间来存储;fortun语言不允许有递归的出现,所以一个函数只能有一个活动记录那么大的空间
2. 当一个函数结束的时候局部变量是否需要保存
一般来说,一个函数的局部变量在函数调用结束的时候所占用的存储空间都要被释放掉,但是有一类语言的特殊语句,允许函数中的某个局部变量保存着,下一次再调用这个函数的时候,这个局部变量是已经具有上一次语言的值。
比如,fortun语言中有一个save语句,save x
;第一次f调用的时候x=100
,下次再进入到函数的时候x的值就是100,这种情况也影响着空间分配的策略。
3. 是否可以访问非局部的变量
比方说外层的变量,还有其他的变量是否可以访问,不同的语言也有不同的约定
4. 函数的参数传递形式
常用的参数传递方式是值引用和地址引用,这就决定了地址引用型的实参只能是一个变量而不能是一个常数,形参的值引用和地址引用的空间分配的方式是不一样的
(因为要传地址,常数的话是不能改变值的)
5. 函数是否可以作为参数进行传递
pascal语言中函数的过程都是可以作为参数进行传递的;在C语言中是函数指针的方式进行传递的。
6. 函数的结果是否可以为函数
属于高阶函数,一个函数的结果还是一个函数
7. 动态的申请存储块
因为可以动态,所以会对操作产生一定的影响
8. 是否可以显式的释放存储空间
pascal中可以new,然后可以dispose显示的释放这个空间,这样操作起来更方便,有的语言中不允许,必须是在函数结束的时候才可以释放这个空间。
三、存储管理模式的分类
静态存储管理对于带有递归方式的语言就不太可以,递归函数的局部变量要占用一串存储单元,这个静态分配无法做到,
四、静态存储分配
完全采用静态分配策略的语言必须满足以下约束条件:
- 不允许递归过程。
- 不允许可变体积的数据,即数据对象的长度和它在内存中位置的限制,必须是在编译时可知。
- 不允许动态建立的数据结构(如动态数组、指针等),因为没有运行时的存储分配机制。
五、动态存储分配
动态存储分配,可以分为:
- 栈式。是一种最常见的模式,在有函数调用的时候动态的分配存储空间(每次函数调用给它分配和过程活动记录相同大小的空间);若程序运行过程中有动态进行申请和释放程序空间,用栈式的就不行了,动态的申请和释放要随时可以找到AR的位置,但是栈式只能从栈顶弹出进行处理。
比方说程序中允许有指针存在,即允许动态申请空间,new(p)这样在静态的时候就没有办法确定他所申请的空间有多大,就没有办法静态的在活动记录里给它分配空间,因此就会导致原来栈式的就不太适用了,因为每一个活动记录的大小是固定的,动态申请空间就会导致没法预知一个活动记录占多大的空间,就导致了所谓的栈式和堆式混合式管理的一种模式。
- 栈堆混合式。活动记录的大小确定的存在栈区中,动态申请的空间在堆区中申请然后释放,同时增加一部分对堆区的管理程序(比方说那块区域是空闲的、申请空间多大 涉及到一些分配策略等等)。
六、栈式管理中的过程活动记录
1. 过程的活动记录:
为管理过程、函数的一次活动所需要的信息,目标程序要在栈区中给被调过程分配一段连续的存储空间,以便存放该过程的局部变量值、控制信息和寄存器内容等,称这段连续的存储空间为过程的活动记录,简称活动记录(Activation Record),并记为AR。
1. 动态链指针记录前一个过程活动记录的sp
2. 返回地址是返回时下一条目标代码地址
3. 返回值是函数的返回值(CALL ,f ,true ,t2) t2的地址
4. 层数
5. 寄存器状态,当前目标机的各个寄存器的值
6. 大小
7. 变量访问环境:访问其他函数中非局部的数据
⚠️针对这个活动记录可以看出,变量、形参所对应的存储位置都是在这个活动记录中,变量的抽象地址形式是一个二元组(层数,偏移),层数是指函数声明的层数
,偏移都是针对活动记录而言的,都是从活动记录的起始
2. 在栈区中通常要设立两个指针: 一个sp是指向当前活动记录的起始位置
,top指向第一个可用的存储单元
。
3. 为什么用两个指针?
因为栈区中元素大小是不同的,不同的函数占用的大小不同,如果不用两个指针表示,那么退栈的时候就没办法退栈了,所以使用两个。 通常在实现的时候会有两个寄存器专门存sp和top,因为经常有运算,把它们放在寄存器里,运算速度比较快,所以从目标机选两个寄存器,专门存储。
🐷多变量共享同一个存储空间
位置,到偏移后的位置,实际上我如果知道了起始位置,又有偏移 一加就知道了变量对应的存储位置。
七、地址分配原则回顾
- 值引用的形参按照类型大小分
- 地址引用的形参分配1
- 局部变量按照类型长度分
- 临时变量分1
- 函数和过程作为形参分2,其中1个是
实参的入口地址
,另一个是先行的display表地址
八、过程活动记录的申请和释放
(一)具体过程:
遇到函数过程调用时申请地址,具体来看在遇到call四元式中间代码时,要生成相应的目标代码。要做的工作有两个:
- 产生一个新的活动记录,即sp=top,top=top+size(当前活动记录大小——在函数的信息表中找到的,符号表中函数信息表,有一项是函数的大小,拿出来就是size)
- 填写过程活动记录的管理信息,返回地址、寄存器内容、动态链指针等等
释放一个是遇到了return,一个是遇到了函数的结束。要做的主要工作有:
- 恢复现场,将寄存器里的值恢复
- 释放当前活动记录即top=sp; sp=动态链指针
- 根据返回地址创建跳转指令
(二)相关概念:
- 调用链:
调用链是过程名的序列,序列的头是主程序名M(pascal主程序、C语言的main函数)。具体地说:
(M)是调用链;若(M,…,R)是调用链,并且R中有S的调用,则(M,…,R,S)也是调用链。
⚠️对于任一过程(函数)S,其调用链不是唯一的,每个调用链对应于一个动态的过程调用序列
。
⚠️用CallChain(S)=(M,…,R,S)
表示S的调用链,表示当前正在执行的是S的过程体,而M,…,R则是已经开始执行但被中断了的过程。
- 动态链:
如果当前正在执行的是S,并且CallChain(S)= (M,N,…,R,S),则栈的当前内容可表示为:
[AR(M),AR(N),…,AR(R),AR(S)],称它为对应调用链(M,N , …,R,S)的动态链。
表示为:
DynamicChains(S) = [ AR(M),AR(N),…,AR(R),AR(S)]
九、变量的地址映射
-
并列式语言:
相对简单只有0层和1层,可以用两个指针来解决,sp0指向全局量的首地址,sp指向当前活动记录的首地址,根据变量的层数来确定是sp0+off
还是sp+off
。值得注意的是const变量也在静态区中. -
嵌套式语言:
若抽象地址是(L,off)
,如果L等于当前层,则说明所对应的变量处于当前的活动记录中;如果L不等于当前层,就要找到他所对应的活动记录的首地址,为此要构造一个display表,把每一个活动记录活跃的静态外层的首地址都存起来,然后找到L所对应的那一层的首地址加上off就可以了。