作者:郑伟林 审核:崔嘉辉
《细品区块链虚拟机》系列共两篇,包括EVM设计理念及优化和WebAssembly。文章的主要描述对象是区块链虚拟机的设计理念和设计目标。
作为区块链3.0时代的公链代表,EOS的虚拟机是一种高性能的区块链WebAssembly解释程序,而基于WebAssembly的虚拟机也逐渐成为区块链虚拟机的一大派系,下文我们将以EOS的虚拟机为代表来分析基于WebAssembly的虚拟机。
编译器=>汇编语言
为了更好的理解WebAssembly,这里我们从编译器如何生成汇编语言开始讲起。任何的高级语言,不管是C++还是Java,最终都要转化为对应机器架构(例如Intet,AMD的X86或者是AMD)的机器码才能被机器执行,不同架构下的机器码规范是不一样。机器码一般是一系列的二进制字符串,为了方便记忆,每一个架构的机器码有对应的一种汇编语言,由一系列的指令组成,例如ADD R1 R2, MOV R1 R2等等。在相同的CPU架构下,汇编语言与机器码是一一对应的,因此,任何高级语言只要能翻译成对应架构下的汇编码,再通过汇编器便可以转化为对应的机器码便可以在机器上执行。
那么如何从任意任意一种高级语言翻译到众多的汇编语言的一种(依赖于机器架构),其中一种方式就是创建不同的翻译器来完成各种高级语言到汇编的映射。
然而这种翻译的效率实在是太低了。为了解决这个问题,大多数编译器都在中间加多一层。它会把高级语言翻译到一个而这个低层又没有低到机器码这个层级。这就是中间代码(intermediate representation,IR)。
也就是说编译器前端把高级语言翻译到IR,编译器的后端把IR翻译成目标机器的汇编代码。目前与编译相关的工具和项目有很多,这里我们主要介绍与EOS-VM相关的LLVM架构。
LLVM架构
LLVM是Low Level Virtual Machine的简称,本质上是一个编译器框架。LLVM最早是一个开源的项目,它最早的时候是Illinois的一个研究项目。随着项目的不但发展,LLVM的范围的不断扩大,LLVM也无法完全的代表这个项目,只是这种叫法一直延续下来。LLVM的主要作用是它可以作为多种语言的后端,它可以提供可编程语言无关的优化和针对很多种CPU的代码生成功能。此外LLVM目前已经不仅仅是个编程框架,它目前还包含了很多的子项目,比如最具盛名的clang。LLVM的优点是开源,有一个表达形式很好的IR语言,模块化作的特别好。目前已经有基于LLVM这个框架的大量的工具可以使用,具体可以参考这个网址http://llvm.org/。
概括来讲LLVM项目是一系列分模块、可重用的编译工具链。LLVM不同于传统的我们熟知的编译器。传统的静态编译器(如gcc)通常将编译分为三个阶段,分别由三个组件来完成具体工作,分别为前端、优化器和后端,如下图所示。
LLVM项目在整体上也分为三个部分,同传统编译器一致,如下图所示,不同的语言的前端,统一的优化器,以及针对不同平台的机器码生成。从图2我们也可以得到启发,如果想实现一门自定义的语言,目前主要的工作可以集中在如何实现一个LLVM的前端上来。
LLVM的架构相对于传统编译器更加的灵活,有其他编译器不具备的优势,从LLVM整体的流程中我们就可以看到这一点,如下图所示为LLVM整体的流程,编译前端将源码编译成LLVM中间格式的文件,然后使用LLVM Linker进行链接。Linker执行大量的链接时优化,特别是过程间优化。链接得到的LLVM code最终会被翻译成特定平台的机器码,另外LLVM支持JIT。本地代码生成器会在代码 生成过程中插入一些轻量级的操作指令来收集运行时的一些信息,例如识别hot region。运行时收集到的信息可以用于离线优化,执行一些更为激进的profile-driven的优化策略,调整native code 以适应特定的架构。
从图中我们也可以得出LLVM突出的几个优势:
I. 持续的程序信息,每个阶段都可以获得程序的信息内容
II. 离线代码生成,产生效率较高的可执行程序
III. 便捷profiling及优化,方便优化的实施
IV. 透明的运行时模型
V. 统一,全程序编译
LLVM IR提供三种格式,分别是内存里面的IR模型,存储在磁盘上的二进制格式,存储在磁盘上的文本可读格式。三者本质上没有区别,其中二进制格式以bc为文件扩名,文本格式以ll为文件扩展名。除了以上两个格式文件外,和IR相关的文件格式还有s和out文件,这两种一个是由IR生成汇编的格式文件,一个是生成的可执行文件格式(linux下如ELF格式)
I. bc结尾,LLVM IR文件,二进制格式,可以通过LLVM自身的解释器lli执行
II. ll结尾,LLVM IR文件,文本格式,可以通过自身的解释器lli执行
III. s结尾,本地汇编文件
IV. out, 本地可执行文件
从上面几种IR格式可以看到,LLVM本身的工具链也提供相应的工具来解释执行IR。此外,LLVM也引入JIT技术(Just-In-Time Compiler),JIT是一种动态编译中间代码的方式。根据需要,在程序中编译并执行生成的机器码,能够大幅提升动态语言的执行速度。
LLVM设计上考虑了解释执行的功能,这使它的IR可以跨平台去使用,代码可以方便地跨平台运行,同时又具有编译型语言的优势,非常的方便。像Java语言,.NET平台等,广泛使用JIT技术,使得程序达到了非常高的执行效率,逐渐接近原生机器语言代码的性能。
JIT引擎的工作原理并没有那么复杂,本质上是将原来编译器要生成机器码的部分要直接写入到当前的内存中,然后通过函数指针的转换,找到对应的机器码并进行执行。实际编写过程中往往需要处理例如内存的管理,符号的重定向,处理外部符号等问题。实现一个LLVM的字节码(bc)的解释器其实并不复杂最好的实例就是LLVM自身的解释器lli,其总共不超过800行代码实现了一个LLVM的字节码解释器,其源代码的github地址为https://github.com/llvm-mirror/llvm/blob/master/tools/lli/lli.cpp
什么是WebAssembly?
在上一小节,我们介绍了编译器如何将高级语言翻译到汇编语言(机器码),那么在上图中,WebAssembly 在什么位置呢?实际上,你可以把它看成另外一种“目标汇编语言”,每一种目标汇编语言(X86,ARM)都依赖于特定的机器架构。当你要把你的代码放到用户的机器上执行的时候,你需要考虑目标机器的架构是怎么样的。而WebAssembly 与其他的汇编语言不一样,它不依赖于具体的物理机器。可以抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。正因为如此,WebAssembly 指令有时也被称为虚拟指令。WebAssembly原本是web领域的一个概念,它比 JavaScript 代码更直接地映射到机器码,它也代表了“如何能在通用的硬件上更有效地执行代码”的一种理念。浏览器把 WebAssembly 下载下来后,可以迅速地将其转换成机器汇编代码。
本质上,WebAssembly一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。更加细节的去讲,WebAssembly(缩写WASM)是一种新的字节码格式,是一种全新的底层二进制语法。突出的特点就是精简,加载时间短以及高速的执行模型。还有一点比较重要,那就是它设计为web多语言编程的目标文件格式。具体可见官网相关介绍:https://webassembly.org/
WebAssembly主要有两种表示形式,其中wasm为WebAssembly的二进制格式,也就是0和1构成的字符串;而wast为对应的文本格式,其与wasm最大的区别就是wast是可读的,有助于开发者理解代码的一种表示形式。wasm和wast是相互等价的,可以通过工具wast2wasm相互转化。
目前对于WebAssembly 支持情况最好的编译器工具链是 LLVM。假设想从 C 语言到 WebAssembly,我们就需要LLVM的子项目clang 前端来把 C 代码变成 LLVM 中间代码。当变换成了 LLVM IR 时,说明 LLVM 已经理解了代码,它会对代码自动地做一些优化。为了从 LLVM IR 生成 WebAssembly,还需要后端编译器。在 LLVM 的工程中有正在开发中的后端,而且应该很快就开发完成了,现在这个时间节点,暂时还看不到它是如何起作用的。还有一个易用的工具,叫做 Emscripten。它通过自己的后端先把代码转换成自己的中间代码(叫做 asm.js),然后再转化成 WebAssembly。实际上它背后也是使用的 LLVM。
wasm目前主要的应用领域在于web应用,对于EOS其将作为智能合约的最终格式,其目前运行在EOS提供的虚拟机上,其机制不同于目前浏览的运行和调用方式。首先我们先简单了解一下wasm是如何在浏览器中运行的,而关于EOS虚拟机如何运行智能合约的分析将在后面进行。浏览器运行的示例:https://webassembly.org/getting-started/developers-guide/,在这个示例可以看到利用emcc的工具生成的最终代码,其中主要有wasm文件,js胶水文件和html 调用文件。
EOS虚拟机(EOS-VM)
EOS中的智能合约概括的来讲就是对多个输入来组织商议输出的过程,EOS中的合约不仅仅可以实现例如转账的这种经济行为,也可以描述游戏规则。EOS中的合约作为注册在EOS区块链上的应用程序并最终运行在EOS的节点上。EOS的智能合约定义了相关的接口,这些接口包含action,数据结构和相关的参数,同时智能合约实现这些接口,最后被编译成二进制格式,在EOS中为wasm,节点负责解析字节码来执行对应的智能合约。对于区块链而言,最终存储的是智能合约的交易(transactions)。
EOS中的智能合约采用C++编程,由两个部分组成分别为action集合和类型的定义:
I. action集合,定义和实现了智能合约的行为和功能
II. 类型定义,定义了合约需要的内容和数据结构
那么,如何把C++代码转化为EOS虚拟机支持的格式。EOS虚拟机目前支持加载wast和wasm两种格式的智能合约。其智能合约的编译主要过程如下:
I. 利用clang以wasm32为目标,生成中间文件bc
II. 利用LLVM-link链接上一个步骤生成bc文件和标准库bc文件生成link.bc文件
III. 利用LLVM的llc生成s汇编文件assembly.s
IV. 应用eosio-s2wasm工具讲s文件转化为wast文件
V. 应用eosio-wast2wasm工具将wast文件转化为最终的wasm文件
得到wasm或wast之后,便可以利用EOS提供的虚拟机(执行引擎)来执行相应的代码。看到这里,可能大家为会有以下疑惑:对于智能合约,为什么我们不直接用clang生成bc文件,然后修改LLVM自带的解释器lli(前文介绍过代码不超过800行)来实现虚拟机呢?这样不是更简单吗?主要有以下几个原因:
I. 出于对未来的考虑。EOS在智能合约目标格式选择上应该做过一定的考虑,对于wasm的选择可能出于社区支持和实现上的双重考虑,这点在采用LLVM-JIT技术就有所体现。目前对wasm支持的解释容器比较多,方便未来多种虚拟机的接入。
II. 出于多语言支持的考虑。目前很多的高级语言,例如C, C++, Rust都可以转化为wasm格式,这给智能合约多语言编程的愿景提供了很好的支撑。
III. 处于对一个良好的生态的考虑。理论上,LLVM中的bc也可以作为智能合约的语言,通过修改lli来完成虚拟机的实现,而且工程实践更加简单,但是问题就是和LLVM绑定了,虚拟机只能和LLVM混,这个限制太大。
EOS虚拟机的发展历史
EOS虚拟机是一个逐渐发展的过程,经历了从无到有阶段。下面我们将分析EOS虚拟机的发展历程。
在EOS的最早期,智能合约采用的是编译执行的执行方式,即直接将代码编译为可执行的二进制机器码发布至节点上直接执行。采用编译执行的方式可以大幅度的提高执行速度,但是由于编译成二进制后,并没有一个虚拟机在当中执行,宿主程序很难控制代码流程。那么如何防止恶意代码的执行?EOS的做法是在转换二进制机器码之前,注入很多检查时间的代码,超时则会抛出异常并结束,因此这个也会成为机器码速度下降的原因之一。此外,在执行是,EOS通过限制合约使用的CPU,RAM等资源来限制代码的执行。此外,由于节点的机器可能不一样,最终编译出来的二进制机器代码可能会有所差别,可能导致了执行结果不一样。EOS刚发布的时候在这问题并没有很官方的解决办法,这种潜在的不一致性是为了效率所付出的代价。
后来,EOS1.0发布的时候,一同发布了基于WebAssembly的EOS虚拟机。之后,EOS上的智能合约便存在两种执行方式。智能合约编译后得到的wasm字节码可以上传至EOS的见证人(节点)。wasm字节码既可以编译成机器码后执行,也可以使用解释器解释执行,如下图所示。
编译执行的优点是速度快,缺点是每次合约有更新时,见证人的服务器都需要重新编译生成二进制机器码,这对于执行次数不多的智能合约是很不划算的。解释执行正好相反,不需要提前编译,但执行速度比编译执行慢很多;EOS的Daniel larimer 创始人说其解释执行仅为原来的20%,也就是比原来慢五倍;不过他也表明,WebAssembly 在整个智能合约执行中只占很小的一部分,对于真正系统性能的影响大约在 5%。此外,引入 WebAssembly 的官方解释器是给智能合约的结果提供了一个权威参考,当各个见证人的编译执行结果不一致时,就可以使用解释器得到参考结果。而且解释器也会给编译执行做后补,以防 WASM 编译器出问题时维持系统稳定。
敲定wasm目标格式后痛苦的事情就来了,目前需要一个能执行它的虚拟机容器。目前都是浏览器支持,落地就是JavaScript执行引擎,如果用JS解析引擎,工程量大,发布还要附带js胶水代码,麻烦的是如何保证结果安全获取。
于是需要的是一个执行wasm的轻量级虚拟机,EOS1.0的虚拟机提供两种执行wasm的模式,分别是基于Binaryen解释器和基于WAVM项目的两种执行wasm的模式。其中,Binaryen的运行模式基于bytecode的解释器,采用边解释边运行的方式,运行速度最慢。EOS1.0是,在Binaryen运行模式的实现上主要参考目前的一个用于WebAssembly的编译工具链项目Binaryen (https://github.com/WebAssembly/binaryen),在EOS github的master分支,基于Binaryen解释器的运行模式的相关代码已经不在,不过你可以切换到1.0.x-releaser的分支,可以看到那个时候EOS是fork了Binaryen(https://github.com/WebAssembly/binaryen) 这个项目来实现基于对wasm的其中一种运行模式。
另外一种执行wasm的模式是参考AndrewScheidecker/WAVM(https://github.com/WAVM/WAVM)项目来实现的。WAVM与Binaryen最大的区别在于WAVM在执行的时候利用了LLVM的JIT技术,通过即时编译大大加快了运行速度,其运行速度目前执行wasm最快的。然而,这种模式有有一个致命的硬伤:JIT时编译速度太慢。在WAVM的实现中,使用LLVM-JIT技术主要还是依赖于LLVM将wasm编译成本地代码。WAVM在构造运行wasm的环境时,需要将wasm编译一次转化为本地代码(存于内存,供运行时使用),这导致了合约的加载时间很长。以编译源代码里的eosio.system这个系统智能合约为例,需要5秒左右的时间。这已经不是慢的问题的,而是在每个块0.5秒的情况下,已经根本无法满足需求了。另外,运行时需要消耗一部分内存来存储编译得到的本地代码,这也导致了WAVM运行模式下合约消耗的内存更多,用户需要出钱购买更多的内存。
之后(2018年9月),EOS1.3发布的时候,又发布了一款新的虚拟机,其底层采用的WABT工具链的解释器 (https://github.com/WebAssembly/wabt) 。 WABT解释器是基于栈的bytecode解释器,EOS官方称WABT解释器使得新的区块链虚拟机相比于1.0版本的虚拟机(基于Binaryen解释器的)在性能上提升了两倍。再到今年的6月初,EOS2.0的发布声称带来了全新的虚拟机(EOS Virtual Machine,简称EOS VM, https://github.com/EOSIO/eos-vm)。
EOS在发布EOS VM的时候曾用下面这一段话概括了他们设计全新的虚拟机的初衷:“随着EOSIO区块链技术的日益普及,支持区块链应用程序的安全确定性执行所需性能已经超过了设计来用于浏览器接口的传统WebAssembly引擎容量。在过去的一年中,我们测试了Binaryen和WABT等现有解释程序的性能,这些解释器非常适合它们本身的用途,但是当应用于区块链时,存在无限内存分配,扩展加载时间和堆栈溢出等问题,导致整体性能和可靠性下降。单线程性能,共享资源跟踪以及对本机代码的低开销调用对区块链性能至关重要。考虑到这些原则,EOS VM从头开始设计,以满足区块链应用的特定需求。“
目前EOS VM出于开发人员预览版本,尚未正式发布。EOS官方声称EOS VM的相对于之前的WABT解释器,执行速度提升了6倍,加载速度提升了20倍,并将于今年晚些时候发布。更多关于EOS VM的设计可以参考https://github.com/EOSIO/eos-vm。
暂无评论