模拟执行
现在来模拟一下 CPU 执行机器指令的情况,由于汇编代码和机器指令一一对应,所以我们可以创建一个直接执行汇编代码的模拟器。
在创建模拟器前,先来讲解一下相关指令的操作。
栈
在内存中,栈的特点是只能在同一端进行插入和删除的操作,即只有 push 和 pop 两种操作。
push
push 指令的作用是将一个操作数推入栈中。
pop
pop 指令的作用是将一个操作数弹出栈。
add
add 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a + b,再将结果 push 到栈中。
sub
sub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a - b,再将结果 push 到栈中。
mul
mul 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a * b,再将结果 push 到栈中。
div
sub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a / b,再将结果 push 到栈中。
四则运算的所有指令已经讲解完毕了,是不是觉得很简单?
代码实现
注意:需要引入词法分析和语法分析的代码
function CpuEmulator(instructions) { this.ins = instructions.split('\r\n') this.memory = [] this.re = /^(push)\s\w+/ this.execute() } CpuEmulator.prototype = { execute() { this.ins.forEach(i => { switch (i) { case 'add': this.add() break case 'sub': this.sub() break case 'mul': this.mul() break case 'div': this.div() break default: if (this.re.test(i)) { this.push(i.split(' ')[1]) } } }) }, add() { const b = this.pop() const a = this.pop() this.memory.push(a + b) }, sub() { const b = this.pop() const a = this.pop() this.memory.push(a - b) }, mul() { const b = this.pop() const a = this.pop() this.memory.push(a * b) }, div() { const b = this.pop() const a = this.pop() // 不支持浮点运算,所以在这要取整 this.memory.push(Math.floor(a / b)) }, push(x) { this.memory.push(parseInt(x)) }, pop() { return this.memory.pop() }, getResult() { return this.memory[0] } } const tokens = lexicalAnalysis('(100+ 10)* 10-100/ 10 +8* (4+2)') const writer = new AssemblyWriter() const parser = new Parser(tokens, writer) const instructions = parser.getInstructions() const emulator = new CpuEmulator(instructions) console.log(emulator.getResult()) // 1138
一个简单的四则运算编译器已经实现了。我们再来写一个测试函数跑一跑,看看运行结果是否和我们期待的一样:
function assert(expression, result) { const tokens = lexicalAnalysis(expression) const writer = new AssemblyWriter() const parser = new Parser(tokens, writer) const instructions = parser.getInstructions() const emulator = new CpuEmulator(instructions) return emulator.getResult() == result } console.log(assert('1 + 2 + 3', 6)) // true console.log(assert('1 + 2 * 3', 7)) // true console.log(assert('10 / 2 * 3', 15)) // true console.log(assert('(10 + 10) / 2', 10)) // true
测试全部正确。另外附上完整的源码,建议没看懂的同学再看多两遍。
更上一层楼
对于工业级编译器来说,这个四则运算编译器属于玩具中的玩具。但是人不可能一口吃成个胖子,所以学习编译原理最好采取循序渐进的方式去学习。下面来介绍一个高级一点的编译器,这个编译器可以编译一个 Jack 语言(类 Java 语言),它的语法大概是这样的:
class Generate { field String str; static String str1; constructor Generate new(String s) { let str = s; return this; } method String getString() { return str; } } class Main { function void main() { var Generate str; let str = Generate.new("this is a test"); do Output.printString(str.getString()); return; } }
上面代码的输出结果为:this is a test
。
想不想实现这样的一个编译器?
这个编译器出自一本书《计算机系统要素》,它从第 6 章开始,一直到第 11 章讲解了汇编编译器(将汇编语言转换为机器语言)、VM 编译器(将类似于字节码的 VM 语言翻译成汇编语言)、Jack 语言编译器(将高级语言 Jack 翻译成 VM 语言)。每一章都有详细的知识点讲解和实验,只要你一步一步跟着做实验,就能最终实现这样的一个编译器。
如果编译器写完了,最后机器语言在哪执行呢?
这本书已经为你考虑好了,它从第 1 章到第 5 章,一共五章的内容。教你从逻辑门开始,逐步组建出算术逻辑单元 ALU、CPU、内存,最终搭建出一个现代计算机。然后让你用编译器编译出来的程序运行在这台计算机之上。
另外,这本书的第 12 章会教你写操作系统的各种库函数,例如 Math 库(包含各种数学运算)、Keyboard 库(按下键盘是怎么输出到屏幕上的)、内存管理等等。
想看一看全书共 12 章的实验做完之后是怎么样的吗?我这里提供几张这台模拟计算机运行程序的 DEMO GIF,供大家参考参考。
这几张图中的右上角是“计算机”的屏幕,其他部分是“计算机”的堆栈区和指令区。
这本书的所有实验我都已经做完了(每天花 3 小时,两个月就能做完),答案放在我的 github 上,有兴趣的话可以看看。
参考资料
我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:谭光志,邀请大家一同入驻:https://www.oschina.net/shari...