English / 中文
WASM-VM 简介
版本 0.7.0
什么是wasm
WebAssembly(wasm) 是一个基于二进制操作指令的栈式结构的虚拟机,wasm可以被编译为机器码,可以更快,更高效的执行本地方法和硬件资源,通过和js协作,前端可以实现更快,更复杂的计算和应用。
不仅可以嵌入浏览器增强web应用,也可以应用于其他的场景。
wasm 支持高级语言编程,目前比较成熟的编译器支持C \ C++ \Rust。
WebAssembly的工作原理
WebAssembly 是一种不依赖于具体物理机器的汇编语言,可以抽象的理解成它是概念的机器语言,而不是实际的物理机器语言,因此,WebAssembly 指令也可称为虚拟指令,可以更快的更直接的映射的到机器码!
编译源码到.wasm 文件
目前对于 WebAssembly 支持情况最好的编译器工具链是 LLVM。有很多不同的前端和后端插件可以用在 LLVM 上。
开发者可以选择 C \ C++ 或Rust语言等开发源代码,再编译成WebAssembly,或者直接使用文本格式的WebAssembly(wast)直接开发。
可以使用 Emscripten 工具来编译WebAssembly,它通过自己的后端先吧代码转换成自己的中间代码(asm.js), 然后再转换成 WebAssembly ,实际上它背后也是使用的LLVM。
实现一个WebAssembly虚拟机
现阶段,WebAssembly 主要还是以Web应用为主,执行的容器大多基于主流的浏览器,并且通过javascript与外部通信,但是它的基于自定义内存和沙盒的特性,也使得WebAssembly 可以很好的适用于一些轻量级的场景,如作为执行区块链智能合约的虚拟机。
WebAssembly 是基于栈式的虚拟机,指令的执行都是在栈内完成的:
webAssembly 指令集参考:webAssembly bianry code
WebAssembly 只支持4种基本类型:
-
int32
-
int64
-
float32
-
float64
所以函数的参数和返回值也只能是这四种类型,并且每个函数只能有一个返回值。
如果想要使用复杂的类型,比如 string,就需要额外对内存进行操作。
初始化内存
当前的 WebAssembly MVP版本,每一个module至多可以拥有一个线性内存(Linear Memory),内存的大小为x * pages, 每页固定为64K Bytes,线性内存本质上就是一个无类型的byte数组,这个数组和物理机的实际内存不存在任何关联,所以在沙盒内的执行的wasm程序不会对外部产生影响。
Data段保存了一些初始化的信息,比如常量字符串。
char * hello(){
return "hello world!";
}
编译(使用Fiddle)后的wast文件为:
(module
(table 0 anyfunc)
(memory $0 1)
(data (i32.const 16) "hello world!\00")
(export "memory" (memory $0))
(export "hello" (func $hello))
(func $hello (; 0 ;) (result i32)
(i32.const 16)
)
)
可以看到:
- 本module 使用了 1页的内存 即64KB
- 在data 段中,“hello world!” 字符串被初始化到偏移量为16开始的内存中
- hello() 的返回值为字符串在内存中的首地址(偏移量),并且将内存 export出来。
这样,外部的调用就可以通过返回的i32.const 16
,在内存中找到以偏移量16开始,”\00”结束的 byte 数组,即为“hello world!”。
如果我们想传入一个字符串参数,内存又该如何使用?
char * hello(char * name){
return concat("hello " ,name);
}
修改代码,传入参数name,并使用concat做字符串连接,编译后的wast代码为:
(module
(type $FUNCSIG$i (func (result i32)))
(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import "env" "concat" (func $concat (param i32 i32) (result i32)))
(table 0 anyfunc)
(memory $0 1)
(data (i32.const 16) "hello \00")
(export "memory" (memory $0))
(export "hello" (func $hello))
(func $hello (; 1 ;) (param $0 i32) (result i32)
(call $concat
(i32.const 16)
(get_local $0)
)
)
)
可以看到,传入的参数类型仍然是i32,即字符串的地址,常量字符串仍然在偏移量为16开始的内存中。
(import "env" "concat" (func $concat (param i32 i32) (result i32)))
因为我们没有在本模块中实现concat函数,编译器自动将其识别为外部函数(参见外部函数部分)。
假如我们的传入参数为 “Alice”,则我们需要在调用hello函数之前,将 “Alice”设置在export 的内存中,并将其地址(偏移量)传入到wasm中。
问题是:如何将传入的字符串设置到内存中?
在浏览器环境下,可以通过JavaScript直接设置,本文不讨论关于WebAssembly在浏览器环境下的使用,类似于Javascript,在ontology-wasm vm中内存是一个可以暴露出来的[]byte,这样我们就可以把参数的字符串转换成[]byte并拷贝到内存[]byte中,并将首地址传入WebAssembly函数,如果是WebAssembly 函数返回的字符串,我们同样可以根据返回的地址在内存中得到实际的字符串。
现在看起来我们已经可以使用这个WebAssembly VM了,但是仍然存在一些问题:
-
不同的编译器对常量字符串在内存中的初始化位置并不相同:如Fiddle是从16开始,而 Emscripten由memoryBase这个传入的global 参数指定。
-
复杂的类型,如struct
struct Example { char * name; char * gender; int age; }; struct Example * p;
当我们需要使用上例的struct 时,就需要为它分配相应的内存:
管理内存
由于WebAssembly VM使用的并不是真正的物理内存,需要我们自己来实现malloc
和calloc
实现对虚拟机内存的分配和管理(ontology-wasm 作为智能合约的执行器,并不需要长时间的持续运行,所以暂时没有加入free()
操作,如果有需要,通过调整内存的页数来增大内存)。
我们将内存分成3个区域:
-
Const Area:保存初始化的常量,从index 16开始,地址0作为NULL的标识。
-
Basic Area :保存基本类型的数据
-
Complex Area :保存复杂类型数据,如struct, array 等。
每次分配内存后,我们需要记录本次分配的信息:
MemPoints map[uint64]*TypeLength
const (
PInt8 PType = iota
PInt16
PInt32
PInt64
PFloat32
PFloat64
PString
PStruct
PUnkown
)
type TypeLength struct {
Ptype PType
Length int
}
key 为本次分配的内存地址,value为本次分配的类型和分配内存的总长度 ,这样我们就可以很容易的根据内存地址来得到实际对应的值的信息。
外部函数
WebAssembly支持引入其他的WebAssembly 模块以调用其中的函数,只需要指定需要调用模块路径即可,如果在本模块内调用的函数并没有具体的实现,就会默认的被认为是从 env中导入。
这样,我们就可以通过注册env 的函数来方便的调用native 方法, 如:
(import "env" "concat" (func $concat (param i32 i32) (result i32)))
我们就可以很方便的以golang来实现这个concat方法,从内存中取得两个字符串,拼接成新的字符串并为其分配和放入内存中,再将结果的地址压入执行栈中。
ontology-wasm vm 实现了一些基本的操作,请参考 Ontology Wasm API list
至此,我们就可以实现一个简单的基于WebAssembly的区块链智能合约执行虚拟机,随着WebAssembly标准的更新,我们也会持续为ontology wasmvm 添加更多更强大的功能。