You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// main.jsvarjit=require('jit.js'),esprima=require('esprima'),assert=require('assert');varast=esprima.parse(process.argv[2]);// Compilevarfn=jit.compile(function(){// This will generate default entry boilerplatethis.Proc(function(){visit.call(this,ast);// The result should be in 'rax' at this point// This will generate default exit boilerplatethis.Return();});});// Executeconsole.log(fn());functionvisit(ast){if(ast.type==='Program')visitProgram.call(this,ast);elseif(ast.type==='Literal')visitLiteral.call(this,ast);elseif(ast.type==='UnaryExpression')visitUnary.call(this,ast);elseif(ast.type==='BinaryExpression')visitBinary.call(this,ast);elsethrownewError('Unknown ast node: '+ast.type);}functionvisitProgram(ast){assert.equal(ast.body.length,1,'Only one statement programs are supported');assert.equal(ast.body[0].type,'ExpressionStatement');visit.call(this,ast.body[0].expression);}functionvisitLiteral(ast){assert.equal(typeofast.value,'number');assert.equal(ast.value|0,ast.value,'Only integer numbers are supported');this.mov('rax',ast.value);}functionvisitBinary(ast){// Preserve 'rbx' after leaving the AST nodethis.push('rbx');// Visit right side of expresionvisit.call(this,ast.right);// Move it to 'rbx'this.mov('rbx','rax');// Visit left side of expression (the result is in 'rax')visit.call(this,ast.left);//// So, to conclude, we've left side in 'rax' and right in 'rbx'//// Execute binary operationif(ast.operator==='+'){this.add('rax','rbx');}elseif(ast.operator==='-'){this.sub('rax','rbx');}elseif(ast.operator==='*'){// Signed multiplication// rax = rax * rbxthis.imul('rbx');}elseif(ast.operator==='/'){// Preserve 'rdx'this.push('rdx');// idiv is dividing rdx:rax by rbx, therefore we need to clear rdx// before running itthis.xor('rdx','rdx');// Signed division, rax = rax / rbxthis.idiv('rbx');// Restore 'rdx'this.pop('rdx');}elseif(ast.operator==='%'){// Preserve 'rdx'this.push('rdx');// Prepare to execute idivthis.xor('rdx','rdx');this.idiv('rbx');// idiv puts remainder in 'rdx'this.mov('rax','rdx');// Restore 'rdx'this.pop('rdx');}else{thrownewError('Unsupported binary operator: '+ast.operator);}// Restore 'rbx'this.pop('rbx');// The result is in 'rax'}functionvisitUnary(ast){// Visit argument and put result into 'rax'visit.call(this,ast.argument);if(ast.operator==='-'){// Negate argumentthis.neg('rax');}else{thrownewError('Unsupported unary operator: '+ast.operator);}}
好了,我们可以运行我们自己的JIT了,原版正装JIT(简陋版) voila(瞧):
$ node ./main.js '1 + 2 * 3'
7
嗯,打完收工,往后出去可以吹牛了。接下来的文章我们会讲讲如何使用堆内存以及浮点数的支持!
The text was updated successfully, but these errors were encountered:
前提
大多数开发人员都是知道JIT编译器的(解释执行,比如ruby JIT, lua JIT- openresty就是集成lua JIT的nginx), JIT可以让我们的解释性语言(一般都比较慢)快如闪电,甚者可以和native code一较高下(当然这有写夸张),但JIT确实是可以让解释性语言跑的飞快。然后很少人知道JIT到底是如何运行的,甚着如何编写一个属于自己的JIT编译器。
掌握一些编译器的基本知识可以帮助我们更好的理解代码的运行原理。
在这篇文章里我们会去揭露一些JIT的原理,甚至实现一个我们自己的JIT编译器。
我们如何开始呢
先确定一个编译器的基本知识点,就是我们可以认为编译器就是把一定格式的输入(通常就是源代码)转换到其他格式或者是相同格式的输出(通常是机器码)。JIT 编译器也不例外。
那是什么让JIT编译器与众不同的呢?那就是JIT并不是提前进行编译的(就是再运行之前编译的,想想我们的golang你想运行golang就得先编译再运行,再比如gcc, clang 或者其他这些都是提前编译的),JIT是在运行时进行编译的(Just-In-Time, 当然也是在执行编译器的输出之前,这句话很怪吧)。
在开始开发我们的JIT compiler之前我们得先选择一个输入语言。我选择的是JavaScript, 他的语法很简单。甚至我会使用JavaScript本身来实现一个JavaScript的JIT。你可以叫他 META-META!
AST (抽象语法树)
我们的编译器可以接收JavaScript的源代码做为输入,然后生成X64架构的机器码(并且立即执行)。虽然人类更乐意使用文本表示,但是在生成机器码之前编译器的开发者则更倾向于使用多种IR(Intermediate Representations)来表示程序。
因为我们写的是一个简化版的编译器,所以我们只有一种IR, 当然这对我们来说足够了。我在这里会采用AST(Abstract Syntax Tree) 来作为我们唯一的IR。
从js的源代码中获取AST很简单,因为有很多现成的js parser他们都可以输出AST。 比如:esprima, acornjs等等。在本文中我推荐使用esprima, 因为他有很好的输出格式(MDN定义的一种AST格式)。
举个🌰, 我们看这句话: obj.method(42) 。 使用esprima.parse(...), 会生成如下的AST
机器码(下面的汇编是x86-64的汇编,不同架构的汇编是各不相同的)
我们先总结一下目前的情况:js源码(ok), AST生成(ok),机器码(待完成)
接下来我们会将一些汇编的基础知识,当然如果你对汇编有所了解的话,就直接跳到下一章吧。
汇编语言其实就是机器或者说CPU能够执行的二进制代码的近文本表示, 考虑到处理器是一行一行的读取并且执行指令的,因此我们把汇编的每一行都看作是一条指令也是合理的,如下:
这个段汇编代码的执行结果是3(你可以从寄存器rax中拿到结果)。通过这个也可以看出来处理器的工作方式:1.把数据放到CPU的(寄存器)里面 2.通知CPU进行计算
通常来说CPU有足够多的寄存器来存放中间结果, 但是在某些情况下你可能也需要使用计算机的内存来存储或读取数据(一股计算机组成原理的味道):
寄存器使用名字来标识(rax, rbx),内存使用地址来标识。地址的标识方式通常是[…]的形式。举个🌰,[rbp-8] 的意思是:从寄存器rbp中取出数据,然后减8,把这个结果作为内存的地址,通过这个值就可以对内存中[rbp-8]这个地址进行读写操作了。
rbp这个寄存器是用来存储当前栈的起始地址的,由于栈地址是从大到小,所以起始地址最大,依次相减就能获取可用的栈空间地址。
在往下讲就会牵扯更多的相关的知识了,我们就此打住。
机器码的生成
实现一个完整的js JIT太复杂了,我们先挑点儿简单的操作一下,就是实现简单的js的加减乘除。
实现js的加减乘除最简单也是最好的办法就是使用深度遍历遍历AST,然后给每个节点生成机器码。那么怎么使用js来生成机器码呢?毕竟使用js时没有办法直接操作内存。
给大家介绍一下jit.js。 这是一个node.js的包(实际上是一个C++的扩展)。这个包可以生成并且执行机器码:
jit.js的原理
X86 机器码的转换 参见
按照上面的方式书写汇编语言,然后转成x86对应机器码, 比如:mov => 0xb3 (这只是个例子映射对错不论)
mmap
使用mmap在内存中开辟一段空间设置为 可执行 状态,然后把上面x86机器码数据写入(然后将起始地址强制转换为 函数指针,接着调用执行就好了)
这里fn调用toString显示的是native code,原因就是上述所描述的,这个fn的执行体并不是js写的,而是从汇编直接转换成机器码然后写入内存强制执行的。
动工实现吧
最后只剩下遍历AST tree的工作了,不过由于我们只是要实现加减乘除,因此遍历很容易实现。
我会支持一下几种:
数字字面量({ type: 'Literal', value: 123 })
二元操作符:+ - * % ({ type: 'BinaryExpression', operator: '+', left: ... , right: .... })
一元操作符:- (
{ type: 'UnaryExpression', operator: '-', argument: ... }
)我们只支持整数暂不支持浮点数。
我们需要处理表达式时会遍历我们所支持的AST node, 生成能够返回rax中的结果的代码。听起来很简单?在动手之前有一件事需要我们谨记:在我们离开一个AST node时我们需要保证所有的寄存器都是干净的(不能污染其他程序),也就是说我们需要保存我们使用过的寄存并且在再次进入时能够恢复他们以前的数据(因为寄存器不是只有我们在使用,而是所有使用cpu的程序都有可能在使用,因此必须保存现场(即保存执行的上下文),否则再次进入就会丢失状态)。不过这个问题CPU已经替我们想好了就是 'pop'和 'push' 两个命令。
下面就是我们的最终的 js 加减乘除版 JIT了:
好了,我们可以运行我们自己的JIT了,原版正装JIT(简陋版) voila(瞧):
$ node ./main.js '1 + 2 * 3' 7
嗯,打完收工,往后出去可以吹牛了。接下来的文章我们会讲讲如何使用堆内存以及浮点数的支持!
The text was updated successfully, but these errors were encountered: