LC-3编译器的代码生成

ECE 220 Fall 2022 ZJUI Machine Problem 11

Translated by DeepL, formatted and checked by Qiushi Liu.

I do not guarantee the correctness of any part of this translated document.

这项作业要求你使用递归将一个C语言子集的程序描述翻译成LC-3汇编代码。开发这样的代码为你提供了在C语言中使用递归和基于指针的数据结构的经验,这两者都是重要的工具。你还会对从一种形式的程序自动翻译成另一种形式的程序所涉及的方法和挑战有更好的理解。编译器的大部分将提供给你,包括将C代码翻译成抽象语法树(AST)的代码,以及支持LC-3上基本算术和I/O的C库例程。你的重点是将AST转化为LC-3指令的序列。

在你开始编程之前,请完整地阅读这份文件。

关于阅读代码

你不需要阅读提供给你的超过3300行的C、词法分析器、解析器和LC-3代码。提供给你。当然,我们欢迎你这样做,但你不需要阅读它们来完成这个任务。这个任务。所有来自标题等的相关材料都包含在本文件中。此外,你应该知道,大量的代码(大约4500行!)是在你构建编译器时自动生成的。而这些自动生成的代码(如 ece220_lex.yy.c)都不是为了让人阅读的,不要尝试。如果你想删除自动生成的文件,这样你就可以看到原始发行中的内容,输入make clear

C语言的子集

为了简化你的任务,该编译器只支持完整的C语言编程的一个狭窄子集。包括对if语句、for循环、return语句和使用大多数C语言操作符的表达式的支持。复合语句是必须的,而不是只作为良好练习(Compound statements are required as opposed to simply being good practice)。例如:

为该编译器编写的程序可以声明全局变量以及一个子程序(当然是int main())。在main中可以声明局部变量,但在其他情况下不允许声明变量(例如,在复合语句中)。只支持两种类型:int和整数的一维数组。不支持指针类型,而且你只能用括号内的表达式来使用数组的索引。例如:

常量字符串可以使用,但只能作为函数调用的参数,如上面printfscanf例子中的格式字符串所示。

支持大多数C语言操作符,包括赋值(=)、基本算术(+-*/%和取反)、前后增减(++--)、逻辑操作符(!&&||)以及比较(<<===>=>!=)。获取变量或数组元素的地址也是可能的(如scanf所需要的),但几乎没有为你做类型检查。

因此,编译器接受下面的代码(和gcc一样,但伴随有警告)。

上面代码中的printf调用产生的输出是不可预测的,没有什么价值,但是scanf调用传递的是变量i的值而不是它的地址,因此可能会不可预测地改变内存。在上面的代码中,i先前被设置为j的地址,但这样的小把戏令人困惑且容易出错。

其他缺失的功能的例子包括:指针解读、多维数组、结构、枚举、定义新的类型、声明中的变量初始化、除for循环以外的循环、switch语句和按位操作。凡是在本文件AST部分没有明确提到的AST节点类型,都没有包括在语言中,你不必担心实现它。当然,你也不可能在你想使用编译器的C程序中使用这种功能。

代码生成

你的代码必须为main子程序生成LC-3汇编,并将其打印到stdout。如你所知,像main这样的C函数由变量声明和语句组成。变量声明将被翻译成一个符号表,你的代码在为每条语句生成汇编代码时必须使用这个符号表。堆栈框架将已经为你设置好,并在你的代码完成后被撕掉。请确保执行堆栈框架的拆解——不要在你生成的代码中直接插入RET指令。

这个MP并不打算要求你进行大量的LC-3编程。你编写的每个递归组件应该不会比实现系统分解的一个步骤更复杂,事实上,你可能想回顾一下我们在课程早期所讲的条件分解和迭代分解的LC-3实现。

除了语句之外,代码生成中的大部分工作都涉及计算表达式的结果。为了简化你的任务,你被要求使用堆栈来计算所有的表达式。你可能还记得,任何表达式都可以使用堆栈和一些基本规则进行计算。要 "执行 "一个字面操作数,如看到一个数字,就把操作数推到栈上。要 "执行 "一个操作数,从堆栈中取出操作数,将操作数应用于这些操作数,然后将操作结果推回堆栈。

虽然这种方法产生的LC-3代码将是冗长而低效的,但使用堆栈与递归很好地结合起来,可以直接实现大部分的MP。例如,一个加法运算符的代码(AST220_OP_ADD,如后文所述)执行以下操作序列:

操作序列 
生成代码以产生加法的第一个操作数(递归调用)
生成代码以产生加法的第二个操作数(递归调用)
从堆栈中弹出第二个操作数(打印LC-3指令)
从堆栈中弹出第一个操作数(打印LC-3指令)
两者相加(打印一个ADD指令)
将结果推入堆栈(打印LC-3指令)

你产生的LC-3指令必须意识到LC-3寄存器的约定,以及你在代码中做出的可以递归调用它的任何假设。例如,考虑一下如果我们不对表达式使用基于堆栈的方法可能发生的情况:一个给定的二进制运算符可能产生它的第一个操作数并将其放入R0,然后生成代码以产生它的第二个操作数。然而,第二个操作数所需的代码必须避免覆盖R0中的第一个操作数。如果第二个操作数使用的是同一个运算符(例如,1+(2+3))怎么办?使用堆栈可以避免这种复杂性。

LC-3的寄存器惯例如下:

寄存器惯例 
R0-R3可供你的代码使用
R4全局数据指针
R5main的堆栈框架指针
R6堆栈指针
R7JSR/JSRR使用

还请注意,为你提供的汇编例程在被调用时确实改变了某些寄存器的值。(详情见下一节)。

C语言库

为了避免你在LC-3中实现功能,我们编写并提供了一个小型函数库,以支持你在生成汇编代码时使用。该库包括四个可以直接从C语言中使用的接口和另外三个接口,供你在实现基本运算时使用。在C语言中可见的调用如下。

这些调用具有C风格的接口:参数必须以正确的顺序放入堆栈,而返回值将在堆栈中。除了R6(递减以存储返回值)和R7(由JSR改变),这些子程序没有改变任何寄存器的值。正如课堂上所描述的,调用这些函数中的任何一个返回后,调用者要负责从堆栈中删除参数。

printfscanf例程以标准方式工作,但只支持少量的转义:

  
%d打印/扫描作为十进制整数的参数
%%打印/扫描单个%字符
\n打印/扫描换行
\\打印/扫描一个单一的\字符

printf例程返回打印的字符数。scanf例程返回转换的整数数量。rand例程返回 02151 之间的伪随机数,srand例程为rand使用的伪随机数发生器设置种子。

为你定义的算术函数有基于寄存器的接口。这三个函数都是对R0R1的二进制运算,并且都在R0中返回运算的结果。除了R0(返回值)和R7(由JSR改变),这些子程序没有改变任何寄存器的值。这些例程的内容如下:

  
DIVIDER0除以R1,四舍五入,并将结果存入R0
MODULUS计算R0%R1(C语言定义),并将结果存入R0
MULTIPLYR0乘以R1,将结果存入R0

所有这七个库的例程都使用堆栈来存储调用保存的寄存器。

抽象语法树

抽象语法树(AST)是中间表示法的一种形式,被用作编译器前端和后端之间的接口。回顾一下,编译器的前端将高级语言(如C)翻译成中间表示,而后端将中间表示翻译成汇编代码。你在这个任务中的工作是为一个基于AST的中间表示法编写一个LC-3后端。

AST是一个基于指针的数据结构,由代表原始代码中的语句和表达式的节点组成。在你的编译器中使用的结构如下:

AST节点的type字段(ast220_t结构)指定了代表哪种节点。例如,一个AST节点可能代表一个if语句,而第二个节点代表一个变量引用,第三个节点代表一个加法操作。一般来说,AST节点的类型决定了结构中其他字段的含义(或没有含义)。这个规则的例外是next字段,它被用来连接AST节点,代表代码块中的连续语句,以及函数调用中的连续参数。

为你的编译器定义了AST节点的两个一般类别:语句和表达式。编译器支持的C语言的子集是最小的,因此只有五种语句类型是可能的:

image-20221217151940395

上表中没有定义的字段没有任何意义,不应假定其持有任何特定的值(例如,NULL)。唯一的语句类型,从你使用C语言的经验来看可能不是很清楚的是AST220_DEBUG_MARKER,它只是一个调试辅助工具,旨在帮助你进行编程。如果你在你的C程序中加入一个DEBUG(20);形式的语句,一个AST220_DEBUG_MARKER类型的AST节点将出现在它的位置上(value为20),这反过来又能在汇编输出中生成一个标记该点的汇编注释(使用一个已经为你写好的例程)。

表达式节点包括我们的C语言子集所支持的所有运算符,以及整数和字符串常量、变量引用等的节点。最简单的表达式包括常量、变量引用和函数调用,它们的AST类型如下表所示。一般来说,为表达式AST节点生成的代码(递归)应该把表达式的结果留在堆栈的顶部。

image-20221217152222664

AST220_VARIABLE节点很特别,它既可用于表达式,也可用于变量的值和地址。在一般的表达式中,你应该假定变量的值必须被找到并推到堆栈中。当AST220_VARIABLE节点是下一页顶部表格中的一个类型的子节点时,变量的地址就会发挥作用。在这些情况下,你应该直接处理该变量,而不是把它当作一般的表达式(也就是说,不要递归到你生成任意表达式的代码的例程)。

image-20221217152325905

剩下的AST节点对应的是用于算术、逻辑运算和比较的一元和二元运算符。请记住,我们为你提供了乘法、除法和模的汇编代码子程序。

image-20221217152404657

用于比较的LC-3代码生成的实现是作为一个构建模块提供给你的。所提供的实现方法做了以下简化假设:为了所有的比较,产生的代码忽略了补码溢出。如你所知,LC-3 ISA只支持与0的比较。因此,通过从左边减去右边并将结果与0比较来转换比较是自然的。例如,当A=0x4000B=0x8000时,A<B是对的吗?显然不是。B是负数! 另一方面,AB=0x40000x8000=0xC000<0,这是对的。

符号表

一个符号表将被构建并提供给你。如前所述,AST220_VARIABLE类型的AST节点使用name字段来指定引用的变量名称。在数组变量的情况下,节点的left字段将持有一个计算数组索引的表达式。解析器会检查索引表达式是否总是存在于数组变量中,而从不存在于标量(非数组)变量中。有两个符号表例程是你感兴趣的:

symtab_dump例程的调用是为了将人类可读的符号表副本放入汇编代码输出中,但如果你觉得方便制作额外的副本,你可以再次调用它。然而,这些额外的拷贝应该在你提交代码之前被删除。

symtab_dump例程可以找到与给定变量名称相关的符号表条目。我们不允许变量隐藏(对局部和全局变量使用相同的名字)。符号表项的定义如下:

当你生成LC-3指令来操作一个变量时,你必须使用变量符号表项的is_globaloffset字段来定位它。正如你可能记得的那样,全局变量是相对于全局数据指针R4的,而局部变量是相对于main框架指针R5的。

你可以假设所有变量的符号表偏移量都落在 [16,15] 的范围内。大的数组可能会超出这个区间,但当我们测试你的MP时,任何数组的第一个元素都会落在这个区间内。

独特标签

我们为您提供了在您生成的汇编代码中创建和使用独特标签的功能:

当你需要一个标签时,声明一个类型为ece220_label_t*的变量,通过调用label_create获得一个新标签。要打印标签,可以调用label_value,从标签指针中获得一个字符串表示。

开始工作

在完全阅读完本文件后,进入你的mp11目录,输入make,将所有内容编译成一个名为c220的可执行文件。在这一点上,c220编译器将从一个有效的C程序中产生有效的汇编输出,但主子程序的汇编代码实现除了堆栈帧的设置和拆分外,实际上不会做任何事情。

你的代码会进入mp11.c文件。具体来说,你必须完成函数:

请注意,除了mp11.c之外,你不能修改任何文件。我们在测试你的代码时不会使用你的任何其他文件。

现有的代码生成

一些生成代码所需的工作已经为你完成,提供给你的mp11.c文件已经包含了大量的必要结构。那里有大约700行,其中大部分是原型和注释。

代码优化不是这个任务的目标,因此任何合理大小的C代码都有可能突破LC-3分支和JSR偏移量的限制。为了避免汇编失败,只有当你知道分支偏移量在范围内时才可以使用分支指令,而且你必须始终使用JSRR而不是JSR。对于分支,为你提供了一个例程,为任意偏移量的 "branch"(即JMP指令)打印一个适当的指令序列:

分支类型是在mp11.c文件的开头附近定义的。

mp11.c中还包含了几个大的开关语句,用于指导通用语句(gen_statement)和表达式(gen_expression)的递归调用。这些开关语句将每种类型的AST节点指向一个小函数,以生成该类型节点的代码。你必须为这些小函数中的大多数编写代码,但有些(六种比较操作)已经为你提供了一个例子。调试标记函数也包括在内。

前半部分

你必须在截止日期前完成两半的所有任务。分开的目的只是为了指导你如何处理这个问题。我们建议你按照这里列出的顺序来做,这将使你能够检查你的进度。

一旦你完成了这些任务,你的编译器将能够使用涉及加法、减法、取反和标量变量的表达式处理赋值和返回语句。你还可以使用对printf的调用来打印出结果。试着用几个不同的程序来检查程序是否正常。

后半部分

你还必须完成以下任务,尽管我们建议你在进行这些任务之前,先让前半部分工作起来。

评分

细节待定,但大致上85%在功能上,15%在注释和文字上。编译你的代码时产生的任何警告将导致同样的扣分(10%)。

请注意,这些规则与以前的MP基本相同。你的所有代码必须包含在你的库的mp/mp11子目录中的mp11.c文件中。我们不会给任何其他名称的文件打分。

一如既往,你的函数必须能够被多次调用并产生正确的结果,所以我们建议你避免使用任何静态存储(否则你可能会失去大部分/全部的功能点)。

执行编译器

如果你在虚拟机内工作,你必须确保解析器编译器——一种叫做Bison的工具——已经被安装。要做到这一点,请输入

编译器,c220,然后接受来自键盘的代码输入,并将汇编语言输出到显示屏上。要覆盖这一行为,例如,要读取一个程序,对其进行编译,并产生一个带有汇编代码的文件,请输入(例如)以下内容:

然后你可以检查testprog.asm文件--确保只看 "STUDENT CODE "标签之间的内容,因为其余部分是自动生成的库代码。或者你可以用lc3as来组装它,然后用lc3sim来执行它。

你也可以自己编写简单的C语言程序,并通过编译器运行它们。只支持C语言的一个子集,但请尽情享受吧!