65.9K
CodeProject 正在变化。 阅读更多。
Home

编译器和语言 2

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2015年5月5日

CPOL

5分钟阅读

viewsIcon

12467

downloadIcon

186

下载 Expression_Evaluation.zip

引言

我的上一篇关于编译器和语言的文章的续篇,https://codeproject.org.cn/Articles/802873/Compiler-and-language

背景

由于这是续篇,最好先阅读我之前关于这个主题的文章。与之前一样,输入程序通过program.txt提供。

附件中提供了一系列程序,让您对这种语言的语法有一个大致的了解。它们利用了其函数调用、数组、自动变量功能。

您需要扎实的汇编编程背景,因为语言的输出是汇编代码,尽管我生成了一个带有内联汇编的 C 文件,以便我们更容易查看变量的结果。

我们将扩展该语言以支持简单的概念(我们认为理所当然的概念)。与之前一样,语法必须同时支持两个版本:编译器解释器,这允许该语言的用户保持代码一致性。

数组

通过数组索引访问数组使用以下语法支持

z=1
s=20
ARRAY:a1:1+z=24+3*s

这里我们可以看到数组 'a1' 在索引 1+z 处写入,写入的值是 24+3*s

类似地

d=ARRAY:a1:z*2

这里我们读取数组 a1 在索引 z*2 处的值

AUTO(栈上的变量)

这些变量的作用域仅限于函数调用,即它们的作用域仅限于定义它们的LABEL直到程序遇到的RETURN

AUTO 变量的使用

AUTO:a=10*20

这将创建一个自动变量 'a' 并为其关联值 200

c=AUTO:a

这将从 AUTO 变量 'a' 中读取值,此变量必须存在于函数调用的作用域内,否则生成的汇编代码将不正确(我们总是可以为此放置一个简单的检查)。

一个优化:尾调用:http://en.wikipedia.org/wiki/Tail_call

此优化适用于 VS2012 发布版本和 gcc -o3 优化

这是一个简单的优化,它检查是否可以忽略额外的栈帧创建,并将为“call”生成的代码替换为“jmp”(这省去了将返回地址推入栈的麻烦),从这种优化返回会强制代码返回到之前的函数。

使用代码

与我之前的文章一样,必须始终参考代码并进行调试,以便更好地理解其工作原理。

让我们看看数组支持

我们首先通过DIM:a1:10声明数组,这会创建一个大小为 10 的一维数组,这在编译阶段是必需的,因为此数组大小必须全局声明,对于解释器,这将被忽略。

为了处理数组,我们需要 2 个临时变量__TarrayIndexProcessing_ , __TarrayValueProcessing_。这些变量用于计算和保存数组索引以及该索引处的值。您会注意到,编译器的索引处理多了一个步骤:乘以 4,因为int的大小是 4 字节,以便正确访问所需的元素。。这对于解释器来说不是必需的,因为它使用

struct arrayvar{
 string m_name; 
 int index;
 DWORD sz;  //maintains the size of the whole array (only one instance is actually required per name)
};

map<arrayvar,int> arrayList;

请注意,arrayvar(解释器使用)使用索引和名称来唯一标识一个元素,请参阅代码以查看映射所需的比较器函数。

AUTOs (栈上的变量)

解释器中的栈上变量相当直接,我们必须为每个函数调用维护栈帧。

这通过以下代码行完成

stack< map<string,pair<UINT,int>> *> stackFrame; 

map<string,pair<UINT,int>> &autoStack=*stackFrame.top(); //maintains the current stack frame

栈帧在程序开始时创建,并且我们始终指向栈顶,当遇到函数调用时,会创建一个新的栈帧并更新栈顶,这确保了当前栈始终保持。

请参阅解释器的 CALL 功能实现。

同样,RETURN: 功能会弹出栈并更新栈顶。

编译器的栈实现并不是很直接,因为代码必须多次解析才能在当前 LABEL 作用域内找到该变量的定义

如果在 LABEL 作用域内找不到变量,则通过调用push element创建一个变量

下面的函数从源代码中查找自动变量,因为我们处于编译阶段,源代码可用于查找相对于 LABEL 关键字的索引。

int getVariablesIndex(string varName, UINT uiLineNumber)

如果未找到变量,则返回-1,否则返回栈帧上的索引(如果找到变量)

例如

AUTO:a=10
b=20
AUTO:c=30

自动变量 'a' 将在索引 1 处
自动变量 'c' 将在索引 2 处
访问这些变量将通过 EBP(此寄存器用于保存帧指针)
我们从不使用 ESP,因为它的值在遇到 push/pop/call 时可能会改变
a 将使用 EBP-4 访问,
c 将使用 EBP-8 访问
 

尾调用:-

这种优化对于编译器和解释器是相似的。

在处理 CALL 后,我们解析文件以查找紧随其后的 RETURN,我们忽略使用 '#' 生成的注释。如果 RETURN 紧随 CALL 之后(忽略注释)被发现,我们知道可以通过放置 'JMP' 而不是 'call' 来尾调用优化此代码,如果使用了 AUTO,则在 RETURN 之前需要额外的代码进行栈清理,并且不会执行尾调用优化。

如果 RETURN 没有立即遇到,则代码必须返回到 CALL 之后的某个点,以执行 RETURN 之前找到的语句,因此将使用 'call' 而不是 'JMP'。

此优化在评估 'CALL' 语言关键字的地方进行编码,请参阅代码。

例如:-

上述程序将输出以下显示的汇编代码,请注意 JMP f1 代替了 call f1

f:
jmp f1
f1:
ret

上述程序将输出以下显示的汇编代码,请注意使用了 call f1,因为没有使用尾调用优化

f:
mov EBP,ESP
sub EBP,4
call f1
mov EBP,ESP
add EBP,4
MOV EAX,10
MOV a,EAX
ret

代码用法

以下输入程序:-

CALL:f
BREAK
LABEL:f
AUTO:a=10*20
CALL:f1
c=AUTO:a
# c 将保存值 200
RETURN
LABEL:f1
AUTO:a=10*30
c=AUTO:a
RETURN

请注意,已调用 'f',使用了 AUTO 变量 'a',然后调用了 'f1',并创建了一个新的 AUTO 变量 'a',请注意在编译输出中,栈将被清理,当 'f1' 返回时,AUTO:a 将正确指向属于其作用域的变量,因此 c 将保存值 '200' 而不是 '300'。

关注点

本文介绍了 ARRAY 和 AUTO 在编译器和解释器中的实现。我们还看到了 EBP 在维护栈帧中的积极使用,以便正确访问栈变量。

© . All rights reserved.