语法、标准库、编译器、汇编器、指令集和虚拟机

〇. 前言

大约在7月24日, 受到某个人写的一个执行汇编并且显示出执行情况的程序的启发, 我也想写一个可以执行汇编的程序。

于是就想了想架构, 觉得这个应该分为如下几个部分:

1. 时钟发生器

每隔一段时间发生一次时钟事件, 调用一次时钟的回调函数。

采用了延时, 后来性能分析发现延时导致性能下降很厉害, 所以当频率很大(大概是2^32-1)的时候不延时了。

2. 内存

用于存放程序、整数数据和浮点数据。

3. CPU

用于执行指令, 后来还添加了中断。

指令的实现使用一个字典来存放, 键就是指令的名字(是字! 符! 串!), 值是一个Action对象, 可以通过调用它来执行指令。

这个字典造成了很大的麻烦。

其实还有别的执行指令的方式, 不一定要用字典……

4. 机器

控制所有乱七八糟的东西。

写好之后, 手写汇编来测试, 觉得很麻烦, 于是添加了一些方便撰写汇编的指令。

还是觉得麻烦, 干脆用ANTLR生成一个解析器, 然后做一个编译器。

编译器做好之后, 有的指令不需要了, 就可以删除了。

这就是在编译器不发达的时候的复杂指令集(CISC)到现在编译器技术发达了的精简指令集(RISC)吧O.O

于是就可以开始撰写好玩的测试程序了。

写了一个递归来计算斐波那契数列的程序, 计算第30项, 伪代码是这样的:

fab(1)=fab(2)=1

fab(x)=fab(x-1)+fab(x-2)

然后发现运行的极其的慢, 比用c++写的慢很多很多。

正好利用新学到的VS中的性能分析来分析一下, 发现执行入栈指令的时候, vb的IsNumeric函数很慢, 于是直接手动判断第一个字符来判断这个字符串是否为数字, 这样性能提升了一点点。

再次运行性能分析, 发现字典判断是否存在一个键的操作导致性能下降, 那只好使用直接用数字为键, 也就是数组, 来获取Action执行指令了。这样, 只需要判断数组下标是否越界即可。

数字为键! 这就是操作码! 而我当初设想的是直接解析执行命令, 但是这实在太慢了。

于是就决定写一个汇编器, 把汇编提前变成操作码存在一个二进制文件里面, 加快执行速度。

为了调试, 有出现了个反汇编器。

一. 指令集

主要使用栈来操作。

比如, 执行加法的时候, 先把两个数字入栈, 然后执行add指令。

add指令会弹出栈顶两个元素把它们相加然后把和压入栈。

然后有各种乱七八糟的指令

nop, hlt, ldri, ldrf, stri, strf, popi, pushi, popf, pushf, and, or, xor, not, addi, subi, muli, divi, negi, inci, deci, addf, subf, mulf, divf, negf, incf, decf, gti, gtei, lti, ltei, eqi, neqi, gtf, gtef, ltf, ltef, eqf, neqf, shl, shr, jmp, jt, jf, call, ret, pushreg, popreg, randi, randf, cli, sei, int, jint, rdtsc, pop, push, add, sub, mul, div, neg, inc, dec, gt, gte, lt, lte, eq, equ, equi,neq, ldr, str, jtrue, jfalse, j, jcall, rand, jmpint, jmpwhenint, jmpwheninterrupt

如上是实现了的指令, 有些指令是别的指令的别名, 是交给汇编器处理的。

大概就是addi(整数加法)什么的变成了add。

二. 语法

直接抄袭C的语法, 手写了个.g4文件, 使用ANTLR生成语法分析器, 十分方便。

为了后文描述方便, 这个语言的名字叫做myl。

三. 编译器

根据ANTLR生成的语法分析器, 写了个语法解析树遍历器, 这样就可以很方便的生成汇编。

四. 汇编器

将字符串变成数字大概就是汇编器要干的事情了。

如前言所说, 为了调试, 就需要个反汇编器, 就是把数字变成字符串而已。

五. 虚拟机

大概就是前言中的架构, 不过后来还添加了一个简单的调试器, 还有就是利用中断来控制磁盘读写的功能。

六. 标准库

为了减少编译器的工作量, 有些操作没有交给编译器来生成汇编, 而是手写了标准库。

使用myl内联汇编来实现的。

比如开启中断就可以写成asm(“sei”);

七. 我想到的执行指令的方法

假设有N种指令

最容易想到的是用Select(c中switch)来判断指令种类, 然后执行, 不过这似乎在最坏的情况下要比较N次。

因为指令编号连续, 所以可以用二分法, 这样就变成了log2(N)次比较。

另外, 可以用数组存放实现这个指令的函数的地址, 直接调用, 不过多了一次函数的调用。

八. 其它

vs的计算代码度量值告诉我, 我的虚拟机程序一共有1212行代码, 不过大概看了一下, 这应该指的是有效代码行数。

  • 代码的行数 – 指明代码中的大概行数。 该计数基于 IL 代码,因此并不是源代码文件中的确切行数。 计数过高可能表示某个类型或方法正在尝试执行过多的工作,应予以拆分。 还可能表示该类型或方法难以维护。

然后编译器有612行代码(不算ANTLR自动生成的)。

另外, 实现CPU的某个方法出现了三角! 意思是可维护性很差! 这个方法有500+行代码! 惊悚! 惊悚! 应予以拆分!

是因为我把所有CPU指令的实现写在了一个函数里面:)

PS. 今天做完了物理和化学的作业, 提前完成计划。

发表评论?

2 条评论。

  1. 条理清楚,具有软件架构师的潜能 :)

回复给 kjr ¬
取消回复

注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

:wink: :twisted: :roll: :oops: :mrgreen: :lol: :idea: :evil: :cry: :arrow: :?: :-| :-x :-o :-P :-D :-? :) :( :!: 8-O 8)

本文链接:https://twd2.me/archives/6112QrCode