在《D Parser 之前:写一个简单的虚拟机》里,其中计算 1 到 100 之和的程序 add.bin,是使用十六进制编辑器直接编辑出来的。虚拟机制作完后,考虑了一下,如果直接写 Z 的编译器,难度还是不小,所以决定,先写一个汇编语言的编译器,实现从汇编代码到机器代码的编译工作。
大体来说,汇编编译基本上是一条一条对照生成,不过,行号的需求使得其中多了一些复杂性,另外,我还决定加入注释的支持。所以,这也是一个比较好的机会实践一下分析器生成器的使用。
汇编语言部分做了少量修改,over 改为 end,行号改为加 @ 前缀,完成的 Grammatica 的分析文件如下:
%header%
GRAMMARTYPE = "LL"
DESCRIPTION = "A asm grammar for zvm."
AUTHOR = "Lephone Liang"
VERSION = "1.0"
DATE = "7 January 2008"
LICENSE = "."
COPYRIGHT = "Copyright (c) 2008 Lephone. All rights reserved."
%tokens%
EAX = "eax"
EBX = "ebx"
ESP = "esp"
EIP = "eip"
SET = "set"
MOV = "mov"
TJMP = "jmp"
ADD = "add"
TGT = "gt"
TGTEQ = "gteq"
TEQ = "eq"
TNOT = "not"
IF = "if"
TOUT = "out"
TEND = "end"
POINT = "*"
COMMA = ","
NUMBER = <<(-)?([0-9])+>>
LABEL = <<@[a-z]+>>
COMMENT = <<;[^\n\r]*[\r\n]>> %ignore%
WHITESPACE = <<[ \t\n\r]+>> %ignore%
%productions%
Expression = Atom [Expression];
Atom
= SetEax
| MovEax8Esp
| SetEbx
| MovEbx8Esp
| Mov8EspEax
| Mov8EspEbx
| AddEsp
| AddEaxEbx
| Gt
| Gteq
| Eq
| Not
| IfEaxJmp
| Jmp
| Out
| End
| LineLabel ;
SetEax = SET EAX COMMA NUMBER;
MovEax8Esp = MOV EAX COMMA POINT ESP;
SetEbx = SET EBX COMMA NUMBER;
MovEbx8Esp = MOV EBX COMMA POINT ESP;
Mov8EspEax = MOV POINT ESP COMMA EAX;
Mov8EspEbx = MOV POINT ESP COMMA EBX;
AddEsp = ADD ESP COMMA NUMBER;
AddEaxEbx = ADD EAX COMMA EBX;
Gt = TGT;
Gteq = TGTEQ;
Eq = TEQ;
Not = TNOT;
IfEaxJmp = IF EAX TJMP LABEL;
Jmp = TJMP LABEL;
Out = TOUT EAX;
End = TEND;
LineLabel = LABEL;
生成代码后,加入新建的 ZasmC 工程,参照 Grammatica 的例子调试了一会儿,增加一些处理代码后,编译器可以正常工作了。用它编译上一次的的 1 到 100 和的汇编代码,发现几个汇编代码的格式错误 后,编译成功,加载入虚拟机,运行得到结果:5050。
还想再写一个程序验证一下,Fibonacci 序列是一个不错的例子,于是编写 d 的原型如下:
import std.stdio;
static void main(char[][] args)
{
int i=0;
int a=1;
write(a);
int b=1;
write(b);
int t;
next:
t = a + b;
write(t);
a = b;
b = t;
i++;
if(i<10) goto next;
}
void write(int n)
{
writefln("%d", n);
}
改写为汇编代码如下:
; 斐波那契
; esp i, esp+4 a, esp+8 b, esp+12 t
; int i=0;
set eax, 0
mov *esp, eax
; int a=1;
; write(a);
set eax, 1
add esp, 4 ; a
mov *esp, eax
out eax
; int b=1;
; write(b);
add esp, 4 ; b
mov *esp, eax
out eax
add esp, -8 ; i
; int t;
@next
; t = a + b;
; write(t);
add esp, 4 ; a
mov eax, *esp
add esp, 4 ; b
mov ebx, *esp
add eax, ebx
add esp, 4 ; t
mov *esp, eax
out eax
; a = b;
; b = t;
add esp, -4 ; b
mov eax, *esp
add esp, -4 ; a
mov *esp, eax
add esp, 8 ; t
mov eax, *esp
add esp, -4 ; b
mov *esp, eax
; i++;
add esp, -8 ; i
mov eax, *esp
set ebx, 1
add eax, ebx
mov *esp, eax
set ebx, 10 ; 循环次数
gteq
not
if eax jmp @next
end
用 ZasmC 编译,生成 Fibonacci.bin,加载到虚拟机,第一次运行错误,后来发现是 d 转汇编的时候的疏忽,修正汇编代码后,编译,加载运行,得到正确的结果。
下一步就是写 Z 的编译器了,这一步可能要花比较长的时间,准备把 Z 编译成汇编代码,然后再用这个汇编编译器编译成机器代码,这样,Z 编译器就不需要处理行号问题了。
下面是虚拟机和汇编编译器的源代码,以及运行 Fibonacci 的截图: