当前课程知识点:Compilers Techniques > 5 Organization and Management of Run-Time Storage Space > 5.1 Overview > 5.1 Overview
返回《Compilers Techniques》慕课在线视频课程列表
返回《Compilers Techniques》慕课在线视频列表
各位同学大家好
今天我们来学习第6章
运行时存储空间的组织与管理1
可能很多同学会有这样的疑问
我们学的是编译的课程
涉及到的是
程序运行之前的相应的操作
那么我们为什么要学习运行时
存储空间的组织和管理呢?
实际上是这样的
大家可以想象一下
对于一个程序而言,它是静态的
也就是说
当你的程序
从高级语言转到低级语言以后
那只是在代码上
进行了一定的转换
而这样的一个静态的程序
是没有办法运行的
而且我们通过学习程序语言
我们也知道
所有的程序
有可能不是顺序执行的
某些程序段可能存在着
循环多次执行的这样的情况
因此只通过编译
把静态的程序转换成对应的低级的代码
是远远不够的
我们还需要把程序的静态代码
和运行时的活动
给它结合起来
以便在编译的后期
把在运行期间的一些数据准备好
这就是我们第6章
要学习的主要的目的
我们看,通常命令式的编程语言
是由多个函数来构成的
通常有一个函数作为入口
称为 main 函数
在代码执行的过程
实际上是一个
逐渐调用不同函数的这样的一个过程
每个过程的执行
需要一定的存储空间来完成
我们熟知的存储空间就是
过程的代码执行
所需要的存放代码的这样的空间
此外在这一章当中
我们还会介绍到
除了代码之外
还有相应的数据的空间
以及代码调用这个过程
维护调用能够顺利执行的
这样的数据结构和空间
那么对于编译 完成词法、语法等等
这一系列分析之后
我们通常会把常量、变量等等
这些用户定义的量
和程序代码结合到一起
并且在程序执行的过程当中
通过和它对应的存储单元建立联系
以便在执行目标代码的时候
能把数据和代码组织到一起
那么在程序语言当中
所有的存储单元
都是通过标识符来表示的
在词法分析阶段
我们了解到每一个标识符
用同样的记号来表示
它们在字母表里有详细的定义
但是对于不同位置来定义的这样的变量
那最终在内存当中
也会出现在不同的位置
由谁来实现这样的分配呢?
有一些我们说是
通过编译的后期来实现的
这也是我们这一章要了解的内容
对于编译程序来说
内存的组织与管理
是一个比较复杂、而且很重要的
一项研究内容
我们在这一节课当中
重点介绍活动记录这方面的内容
想了解活动记录
就要知道什么是活动
所谓活动,它是针对过程而言的
过程一次执行被称为过程的一次活动
也就是说
大家可以把这个过程看成是一个函数
一个函数被执行了
那么我们就说
它对应的过程活跃了一次
在内存当中生活了一次、
活跃了一次、执行了一次
那么对应活动的记录就叫活动记录
活动记录
顾名思义就是记录
当前过程在运行过程当中
它的一些相关的数据和相应的信息
而存放信息的存储空间
我们就称其为活动记录
在这一节当中
我们主要讨论
活动记录当中的数据安排
也就是说,活动记录究竟记录哪些内容
第二点
在程序执行的过程当中
所有的活动记录的组织方式
每个过程对应一个记录
那么过程和过程之间
都有不同的记录
它们之间是不是有一些联系
我们也会在这节课当中
给大家介绍
那么首先我们看
对于过程来说
我们先要了解运行的过程
有什么样的特点
第一点就是过程它的生存期
所谓生存期
就是指当前过程
从第一次被调用到整个过程调用结束
所存在的时间
这个时间就称为它生存的时间
也称为生存期
如果过程是可以递归的
那么整个递归过程的最外层
也就是第一次被调用
一直到最后一层调用结束
整个它都是在活跃的
那么一个过程
也可以对应多个活动
也就是说
对应一个静态的代码、一个函数
它可以多次被调用
每次被调用
都称其为它活动了一次
这是关于过程的
我们在程序写的时候
会遇到的一些个例
一些问题大家先回顾一下
那么在学习数据的存储分配的过程当中
我们也要和之前
我们学习过的数据结构
以及程序的代码结合到一起
首先大家注意我们的编译
都在逻辑地址空间来讨论
也就是说我们假想
每个程序都完全享有内存
然后如果它究竟对应到
物理内存上是什么位置
这个工作谁来完成呢
如果大家学习了操作系统
会知道这是用操作系统
来做这个镜像来完成的
因此对于编译来说
我们可以很局限的认为
它就是
每个程序都享有自己的内存
那么以上这样的8种方式
就会影响到数据的分配和存储
比如说过程是不是能够递归
过程能不能够访问局部的变量
能不能作为参数被传递等等
那么这些就决定了
我的变量、我的过程的数据
要存放在什么位置
比如说我们来看
过程能否递归
如果是一个递归过程的话
那么它其中的变量
就会反复被使用到
那么每一次使用
其实都会在内存当中
分配新的单元
彼此递归之间的每一个过程
是没有相互影响的这样的数据
那么还有我们看第三点
过程能否访问非局部变量
我们在学程序语言的时候
学过一个全局变量
还有局部静态变量
我们知道每次过程退出之后
原有的这里面原有的值
是不会改变的
这就是说
这个过程是不是可以访问
非局部的、全局的这样的变量
这都和程序语言有着直接的关系
如果想过程退出之后
当前计算的结果数据
仍然保留的话
我们往往用一个全局变量
给它放到全局的静态数据区
因此也就是根据你的需要
不同的数据
可能会存放在不同的位置
这样它的生存期、
它的保存时间就会有所不同
那么我们来看一下
过程它的定义
首先我们看
这样写的一个函数
就可以称其为过程的一个定义
这是PASCAL语言的
这是C语言的
那么所谓过程的调用
就是我们看
对于这个main函数
如果调用 f 的函数的话
这就可以看成是过程的调用
而这里面的 a 和 b 就是形参
对应的
在调用过程当中的 3 和 true 就是实参
这就是一个定义
和调用形参和实参的一个实际的例子
我们再来看一下
我们想要讨论的
变量的存储的相关的问题
我们看在 function 这个函数
function f 这个函数里面
定义了一个全局的变量a
然后在这个函数内部
定义了一个局部的变量 int c
我们在 main 函数里
给 a 做了一个赋值 a =0
我们看这里面用到的就是全局变量
这样使用是没有问题的
所以这是可以的
属于 a 的作用域范围
但是如果在 main 函数里
使用 c=0 的话
这是不合适的
原因是 c 是 f 函数内部定义的
在 main 函数里无权使用
所以这种用法就是错误的
大家在用变量的时候
要注意两者的区别
那么关于名字的作用域
所谓作用域
就是指它起作用的范围
那么即使一个名字在程序当中
只声明一次
那么运行时也可能
有不同的数据对象对应着它
这个数据对象
指的就是对应的存储单元
以刚才我举的我们的递归函数为例
每一次调用
递归函数的静态正文是不变的
它里面的变量是 a 是 b 也不会改变
但是每一次调用在内存当中
都会给它分配新的存储单元
我们看这里面
调用两次 f 函数
第一次给实参是 3
第二次给实参是5
那么两次调用
都会用到这里面的参数 b
那么不同的调用会给 b
对应不同的存储单元
来给它分配
来给它赋值
这是对应不同的存储单元的分配问题
另外我们在写静态正文的时候
写程序的时候
通常你只需要赋值 a 赋值成3
b 赋值成 5 等
这样就可以了
但是对于编译来说
我们需要把每一个变量
它对应的位置给它分配好
也就是说在静态地址空间给它分配好
那也就是说
我们需要把名字和存储单元
绑定到一起
那么这里面介绍两个新的概念
一个叫做环境
环境可以把名字映射到左值
也就是存储单元
那也就是比如说A=5
那么这个A就会绑定到一个存储单元
那么这个 5 改变的是
当前存储单元的内容
但是并不改变环境
也就是不改变绑定
如果环境把名字 X 映射到存储单元 S
我们就说 X 被绑定到 S
我们看这就是它们之间的关系
把 A 赋值给某一个变量的话
那么 A 和存储单元之间的绑定
用环境来表示
这是由编译器来完成的
那么存储单元到值之间的这样的映射
我们称其用状态来表示
那也就是状态可以改变值
但是不改变它们之间的绑定关系
下面我们就来学习
活动记录的相应内容
活动记录是用来存放所需信息的存储空间
这块空间就称为活动记录
编译程序每次被运行时
编译器就会从操作系统
获得一块内存
这块内存基本包括以下三方面内容
第一方面
就是编译之后的目标代码
也就是我们说的
低级的这样的代码程序
那么还有数据对象
有了代码需要操作数据
那么数据对象也会占有一定的空间
第三部分就是用于管理过程
活动的控制栈
也就是活动记录的内容
对于前两项我们可以想象到
之前也了解过
我们想一个程序能运行
要有代码、要有数据
但是大家比较陌生的
应该是第三部分
也就是活动记录
也就是说一个程序想要运行
光有代码和数据是不够的
因为它们之间是一个动态的过程
一个静态的程序可能会被反复执行
它们之间存在跳转
这些由谁来维护呢?
实际我们要讲的活动记录来维护
那么活动记录
相应的存在的位置
一般会存在栈里面
对一个内存划分来说
我们可以用这样的一个图来了解
代码会放到代码区
静态的数据放到静态数据区
我们在学习语言的时候
我们知道
自己可以申请空间放到堆区
然后活动记录在栈区
堆和栈是相向增长的
这是它们分别存放的内容
我们以一个例子来看
这里面定义了一个函数 p
然后在 main 函数里面
调用这个 p 函数
那么它的活动记录安排
就是在栈里
main 函数会被先调用
那么就会先出现 main 函数的活动记录的内容
进而调用到 p 的时候
p 的活动记录就会在栈里出现
它就好像在一个柜子里摞衣服一样
谁先出现谁先进栈
然后被调用到的程序执行的时候
它对应的活动记录就会出现
活动记录的一般布局
我们看一般有以下几项
以下7项
那么这几项它的分配是
首先被压栈的会是实参
也就是说
当一个函数被调用的时候
传递给它的实参会被先压栈
进而压的是返回值
这个返回值实际上
只有等程序运行结束之后才能拿到
因此这里面
放的是根据返回值的类型预留出来的空间
int 就放4个字节
就是这样的一个过程
接下来压栈的是控制链
控制链实际上是一个指针
对于32位机来说
它就占4个字节
它用来指向调用过程的指针
它的作用是什么呢?
是一个指针
一定是存放了一个地址
这个地址指的就是谁调用我当前的过程
那么我存的就是
调用我的那个过程的活动记录的指针
也就是说
通过整块活动记录的这一项
我就可以找到
调用我的那一块活动记录
这就是控制链的内容
接下来是访问链
访问链在当前的高级语言当中
用的比较少
在早期的PASCAL语言当中
用的比较多
用于存放其它活动记录的非局部数据
也就是说
如果我想访问的数据
不在我函数内部
我想访问其它的
我就可以通过访问链
这里放的也是一个地址
去来找我的外层
接下来是机器状态
机器状态可以用来保存
这个过程调用前的机器状态
比如说一些寄存器的内容
通常放到这里面
然后局部数据
这个大家应该不陌生
所谓局部数据指的就是
我当前函数内部定义的一些变量
最后是临时数据
也就是说当你的程序写了比较长
逻辑关系比较复杂
在运行过程当中
会产生许多中间的这样的结果
那么(临时数据)就一会增加、一会减少
放到栈顶临时数据区
活动记录的一般的布局
那么我们说这几项不是固定不变的
也就是说根据不同的高级语言
它会稍有变化
位置也会稍有差别
但是总的来说
都是包含以上这几项
了解了活动记录的相应安排
那么在其中, 我们最熟悉的
应该是局部数据
下面我们就来了解一下
局部数据的安排
有什么样的特点
对于局部数据来说
我们知道
字节是可编制的最小的单位
我们在最初学习C语言等等
这些编程语言的时候
我们就会了解不同的类型
它是如何存放的、占多少字节
那么在这个过程当中
对于编译器来说
会按照你所写的程序
出现的变量的先后顺序
在内存当中
也就是活动记录的局部数据当中
来给它分配相应的位置
而局部数据所在的地址
通常是用偏移量来表示的
针对谁的偏移呢?
实际上是针对于活动记录基地址的偏移
这个基地址
通过刚才的讲解
我们知道,一共有7项活动记录内容
那么其中的控制链所在的地方
就是整个活动记录的基地址
那么就以它的偏移
来确定局部数据所在的位置
数据对象的存储和安排
也会受到目标机器选址方式的影响
而且在存储的过程当中
为了存取方便
考虑到效率的影响
往往会有这个空间的剩余
我们称其为衬垫空白区
以上就是本节课的相关内容
谢谢大家
-1.1 Overview of Compilers Techniques
--Chapter 1 Overview of Compilers Techniques
--Overview of Compilers Techniques
-2.1 Lexical Tokens, Strings and Language
--2.1 Lexical Tokens, Strings and Language
--2.1 Lexical Tokens, Strings and Language
-2.2 Regular form
--2.2 Regular form
-2.3 Finite automata
--2.3 Finite automata
-2.4 DFA construction, Subset construction, Simpleset DFA
--2.4 DFA construction, Subset construction, Simpleset DFA
-2.5 Lex
--2.5 Lex
-3.1 Context-free Grammars
--3.1.1 The Role of the Parser
--3.1.2 The Formal Definition of a Context-free Grammar
-3.2 Writing a Grammar
--3.2.1 Elimination of Left Recursion
--3.2 Top-Down Parsing
-3.3 Languages and Grammars
--3.3 Language and Grammars
-3.4 Top-Down Parsing
--3.4.3 Recursive Descent Analysis
--3.4.4 Nonrecursive Descent Analysis
-3.5 Bottom-up Parsing
--Bottom-up Parsing
-3.6 LR Parsing
--3.6.6 Characteristics of LR Parsing
--3.6.7 Non Ambiguous and Not LR Context-Free Grammars
-4.1 Syntax-Directed Definitions
--4.1.2 Attribute Dependency Graphs and Ordering the Evaluation of Attributes
--Syntax-Directed Definitions
-4.2 Bottom-Up Calculation of S Attribute
--4.2.1 Bottom-Up Calculation of S-Attributed
-4.3 L-Attributed Definitions
--4.3.1 L-Attributed Definitions
--4.3.3 Design of Predictive Translator
--L-Attributed Definitions
-4.4 Bottom-Up Parsing of L-Attributed Translation
--4.4.1 Bottom-Up Parsing of L-Attributed Translation
--4.4.2 Simulate the Parsing of Inherited Properties
--Bottom-Up Parsing of L-Attributed Translation
-5.1 Overview
--Overview
-5.2 Global Stack Storage
-5.3 Calling Sequences
-5.4 Non Local Names
--5.4 Non Local Names and dynamic scope
--Non Local Name
-6.1 Overview of Intermediate Code Generation
--6.1 Overview of Intermediate Code Generation
-6.2 Declaration Statements
-7.1 Issues in the Design of Code Generator
--7.1 Issues in the Design of Code Generator
-7.2 Target Machine
--Target Machine
-7.3 Basic Blocks and Flow Graphs
--7.3 Basic Blocks and Flow Graphs
-7.4 A Simple Code Generator
-8.1 Demonstration of Compiler Framework based on Python
--8.1 Demonstration of Compiler Framework based on Python
-8.2 Basic
--8.2.4 SA
-8.3 SimpleJava