0%

94:c4编译器回顾(4)

在上一章中,我们完成了对函数体的解析,现在我们需要进一步解析函数体中的语句,还有程序中的表达式。我们需要将程序中的语句解析成我们的虚拟机能直接执行的语句,所以这一章中会有一点难度:

语句

我们的编译器中识别以下六种语句:

  • if(...) <statement>; [else <statement>;]
  • while (...) <statement>;
  • {<statement>;}
  • return xxx;
  • <;(empty statement)>
  • expression(这个我们稍后单独讨论)

现在我们要将这些语句转换成对应的汇编代码:

IF语句

IF语句的作用是跳转,根据条件表达式决定跳转的位置:

1
2
3
4
5
6
7
8
9
if (...) <statement> [else <statement>]

if (<cond>) <cond>
JZ a
<true_statement> ===> <true_statement>
else: JMP b
a: a:
<false_statement> <false_statement>
b: b:

对应的流程就是:

  • 先执行条件表达式<cond>
  • 如果条件失败,则跳转到a的位置,执行else语句
  • 如果条件成功,则在执行<true_statement>之后,无条件跳转到b,结束判断。

我们的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(token==If){
match(If);
match('(');
expression();
match(')');
*++text = JZ;
b = ++text;
statement();
if(token==Else){
match(Else);
*b = (int)(text + 2);
*++text = JMP;
b = ++text;
statement();
}
*b = (int)(text);
}

我们可以从栈的视角进行解释,text始终是指向下一条要执行的指令的。我们希望JZ后面跟着的是else的代码块的入口或是if判断之后的地址。

这里我们使用了延迟绑定地址的方式,我们来看看以下两种情况:

  • 只有if,我们将在执行完if_statement之后,将JZ之后的跳转地址设置成下一条指令的地址text
  • else,我们希望能够跳转到else的起始地址,我们需要在当前text指向的地址基础上+2,因为我们需要跳过JMP出口地址所占用的指令空间。同时我们需要将if_statement之后的地址设置成判断的出口,即else_statement的后一条指令

While语句

While语句的汇编代码更加简单,如下:

1
2
3
4
5
6
a:                     a:
while (<cond>) <cond>
JZ b
<statement> <statement>
JMP a
b: b:

我们的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
else if(token==While){
match(While);
a = text;
match('(');
expression();
match(')');
*++text = JZ;
b = ++text;
statement();
*++text = JMP;
*++text = (int)a;
*b = (int)(text);
}

在跳转之前用a保存expression开始的指令,用于重新计算cond。然后延迟绑定地址b,作为while逻辑的出口

Return 语句

遇到Return语句则代表函数将要退出了,这一部分很简单,实现如下:

1
2
3
4
5
6
7
else if(token==Return){
match(Return);
if(token!=';'){
expression();
}
*++text = LEV;
}

其他语句

其他语句并不生成汇编代码,所以简单的匹配消耗即可:

1
2
3
4
5
6
7
8
9
10
11
12
else if(token=='{'){
match('{');
while(token!='}'){
statement();
}
match('}');
}else if(token==';'){
match(';');
}else{
expression();
match(';');
}

expresion

我们已经完成了对语句的解释,现在我们需要实现对表达式的解释,在此之前我们要明确什么是表达式?怎么解析我们的表达式,并计算他们。

怎么计算表达式

对于表达式中的运算符,每一个符号都有自己的优先级,在进行运算的时候,我们希望运算符优先级高的子式先被计算。例如在2 + 3 * 4中,我们希望*先被计算,然后再是+。对于我们生成的汇编代码而言,我们应该优先为优先级高的运算符生成目标代码,所以如何确定一个表达式的优先运算顺序十分重要。

这里我们使用递归下降的方法实现对表达式运算符的解析,我们在一开始定义标识符时,实际上就对运算符的优先级进行了排序:

1
2
3
4
5
enum {
Num = 128, Fun, Sys, Glo, Loc, Id,
Char, Else, Enum, If, Int, Return, Sizeof, While,
Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak
};

我们首先明确,一元运算符的优先级始终是高于二元运算符的,所以这里我们需要,首先完成对一元运算符的解析,这里先罗列几个较为特殊的:

  • Num 当我们遇到一个数字时 要返回它的数值
  • "..." 当我们遇到一个字符串时 要返回它的指针
  • PTR 当我们遇到一个指针类型时 我们要正确的解析它的解引用
  • Func 当我们遇到一个函数调用时,我们要正确的执行它,并返回返回值
  • Id 当我们遇到一个变量时,需要返回它的存储的值

然后,对一元预算符的判断结束后,我们需要对二元运算符的解析,但是对于二元运算符,我们需要考虑运算符号的优先级。我们给每一个运算符都设置一个当前的level,每次只对高于/等于当前level的运算符进行解析,每次解析完一个运算符之后,都将当前的优先级提高。这样我们就实现了对运算符的递归下降分析,我们的框架代码如下:

1
2
3
4
5
6
7
8
9
10
11
int expression(int level){
// 解析一元运算符
{
... // 解析顺序按一元运算符优先级进行解析
}

// 解析二元运算符
while(token>=level){
...
}
}

接下来,我们给出具体的实现:

常量

NumIMM指令将其加载到ax中即可:

1
2
3
4
5
if(token==Num){
*++text = IMM;
*++text = token_val;
expr_type = INT;
}

然后是字符串常量

1
2
3
4
5
6
7
8
else if(token=='"'){
*++text = IMM;
*++text = token_val;
expr_type = PTR;
// while(token=='"') match('"');
// 对data段进行地址对齐
data = (char*)(((int)(data) + sizeof(int)) & (-sizeof(int)));
}

这里有两点需要注意,如果你想支持"Hello""World" = "HelloWorld"的语法,那么你要设置while(token=='"') match('"');,这里我不想支持就不开了。

然后是data = (char*)(((int)(data) + sizeof(int)) & (-sizeof(int)));,这一部分的作用是将数据段进行对齐,我们希望每次的data指针都是四字节对齐的,这样可以方便我们进行索引,或是避免了字符串访问越界的可能。

sizeof

这个关键字我们也将其作为一元运算符进行处理,我们需要根据后面参数的类型,并返回它的大小。这里我们只支持Int Char Ptr三种类型,其中Ptr类型的大小同int

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if(token==Sizeof){
match(Sizeof);
match('(');
if(token==Int){
match(Int);
expr_type = INT;
}else if(token==Char){
match(Char);
expr_type = CHAR;
}
while(token==Mul){
match(Mul);
expr_type = expr_type + PTR;
}
match(')');
*++text = IMM;
*++text = (expr_type==CHAR) ? sizeof(char) : sizeof(int);
expr_type = INT;
}

变量与函数调用

由于我们将函数和变量的值的词法分析都是以Id开头,所以我们对他们的目标代码生成也放在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
else if(token==Id){
match(Id);
id = current_id;
// 函数调用
if(token=='('){
match('(');
tmp = 0;
while(token!=')'){
expression(Assign);
*++text = PUSH;
tmp++;
if(token==',') match(',');
}
match(')');
if(id->class==Sys){
*++text = id->value;
}else if(id->class==Fun){
*++text = CALL;
*++text = id->value;
}else{
printf("%d: 错误的函数调用\n",line);
exit(-1);
}
// 清除栈上的变量
if(tmp>0){
*++text = ADJ;
*++text = tmp;
}
expr_type = id->type;
}else if(id->class == Num){
*++text = IMM;
*++text = id->value;
expr_type = INT;
}else{
if(id->class = Glo){
*++text = IMM;
*++text = id->value;
}else if(id->class == Loc){
*++text = LEA;
*++text = -id->value;
}else{
printf("%d: 未知变量类型\n", line);
exit(-1);
}
expr_type = id->type;
*++text = (expr_type==CHAR) ? LC : LI;
}
}

指针取值

说实话有点写不下去了,最近有点忙,心情也不好,写这个的过程中断断续续的,所以先到此为止吧。