Edit me

[TOC]

实现方案

如果要实现一个自己的函数,有两种实现方式,一种是在虚拟机内部直接以内置指令的方式实现,如add函数等,但是这种方式需要修改编译器,并且通用性不够好,另一种就是通过解决外部引用的方式实现,这种的兼容性较好,需要解决的问题就是在内部解析引用的时候实现内部导入。

案例分析

如果我们需要实现一个log函数,那么就需要解决外部符号导入问题,如下面代码:

int main() { 
  Println("hello world");
  return 0;
}

它的wast文件为:

(module
 (type $FUNCSIG$i (func (result i32)))
 (type $FUNCSIG$ii (func (param i32) (result i32)))
 (import "env" "Println" (func $Println (param i32) (result i32)))
 (table 0 anyfunc)
 (memory $0 1)
 (data (i32.const 16) "hello world\00")
 (export "memory" (memory $0))
 (export "main" (func $main))
 (func $main (; 1 ;) (result i32)
  (drop
   (call $Println
    (i32.const 16)
   )
  )
  (i32.const 0)
 )
)

它的wasm文件内容如下:

pct@Chandler:~/go/src/github.com/go-interpreter/wagon/cmd/wasm-run$ wasm-dump -s hello.wasm 
hello.wasm: module version: 0x1

contents of section type:
0000000e  02 60 00 01 7f 60 01 7f  01 7f                    |.`...`....|

contents of section import:
0000001e  01 03 65 6e 76 07 50 72  69 6e 74 6c 6e 00 01     |..env.Println..|

contents of section function:
00000033  01 00                                             |..|

contents of section table:
0000003b  01 70 00 00                                       |.p..|

contents of section memory:
00000045  01 00 01                                          |...|

contents of section global:
0000004e  00                                                |.|

contents of section export:
00000055  02 06 6d 65 6d 6f 72 79  02 00 04 6d 61 69 6e 00  |..memory...main.|
00000065  01                                                |.|

contents of section code:
0000006c  01 89 80 80 80 00 00 41  10 10 00 1a 41 00 0b     |.......A....A..|

contents of section data:
00000081  01 00 41 10 0b 0c 68 65  6c 6c 6f 20 77 6f 72 6c  |..A...hello worl|
00000091  64 00                                             |d.|

解析器在解析文件时需要找到env中的Println函数,然后将hello world放入堆栈,接着调用call函数执行Println函数打印出hello world,那么我们需要解决的就是在虚拟机内部实现env中Println函数的解析部分。

解析流程

在上一章节中我们分析过,文件的解析主要在函数wasm.ReadModule(f, importer)中,它首先循环读取各个分区:

    for {
        done, err := m.readSection(reader)
        if err != nil {
            return nil, err
        } else if done {
            break
        }
    }

然后再去解决导入的函数:

    if m.Import != nil && resolvePath != nil {
        err := m.resolveImports(resolvePath)
        if err != nil {
            return nil, err
        }
    }

这里的解决办法是从env的wasm文件中读取导入的函数:

    for _, importEntry := range module.Import.Entries {
        importedModule, ok := modules[importEntry.ModuleName]
        if !ok {
            var err error
            importedModule, err = resolve(importEntry.ModuleName)
            if err != nil {
                return err
            }
            modules[importEntry.ModuleName] = importedModule
        }
        index := exportEntry.Index
        switch exportEntry.Kind {
        case ExternalFunction:
            fn := importedModule.GetFunction(int(index))
            if fn == nil {
                return InvalidFunctionIndexError(index)
            }
            module.FunctionIndexSpace = append(module.FunctionIndexSpace, *fn)
            module.Code.Bodies = append(module.Code.Bodies, *fn.Body)
            module.imports.Funcs = append(module.imports.Funcs, funcs)
            funcs++

然后再创建新的vm时会将函数列表放入vm中:

vm, err := exec.NewVM(m)
vm.newFuncTable()
vm.funcTable[ops.Call] = vm.call

最后根据堆栈来调用相关函数:

    index := vm.fetchUint32()
    vm.funcs[index].call(vm, int64(index))

函数从module到vm的拷贝过程如下:

    for i, fn := range module.FunctionIndexSpace {
        if fn.IsHost() {
            vm.funcs[i] = goFunction{
                typ: fn.Host.Type(),
                val: fn.Host,
            }
            nNatives++
            continue
        }
        disassembly, err := disasm.Disassemble(fn, module)
        if err != nil {
            return nil, err
        }
        totalLocalVars := 0
        totalLocalVars += len(fn.Sig.ParamTypes)
        for _, entry := range fn.Body.Locals {
            totalLocalVars += int(entry.Count)
        }
        code, table := compile.Compile(disassembly.Code)
        vm.funcs[i] = compiledFunction{
            code: code,
            branchTables: table,
            maxDepth: disassembly.MaxDepth,
            totalLocalVars: totalLocalVars,
            args: len(fn.Sig.ParamTypes),
            returns: len(fn.Sig.ReturnTypes) != 0,
        }
    }

那么这里的问题就是如何在保存现有解析新wasm文件的基础上,跳过对Println函数的解析呢?而且需要根据fn的规则实现一个内部的Println。

工程改造

首先是跳过部分,这部分比较简单,我们在解析外部引用时手动判断env并跳过即可:

for _, importEntry := range module.Import.Entries {
   if importEntry.ModuleName == "env" {
      fmt.Println("Module Name:", importEntry.ModuleName, "- Filed Name:", importEntry.FieldName)
      if importEntry.Kind == ExternalFunction {
         //get the function type
         funcType := module.Types.Entries[importEntry.Type.(FuncImport).Type]
         var code []byte
         code = append(code, 0x41)
         code = append(code, 0x0b)
         fn := &Function{IsEnv: true, Name: importEntry.FieldName, Sig: &FunctionSig{ParamTypes: funcType.ParamTypes, ReturnTypes: funcType.ReturnTypes}, Body: &FunctionBody{Code:code}}
         module.FunctionIndexSpace = append(module.FunctionIndexSpace, *fn)
         module.Code.Bodies = append(module.Code.Bodies, *fn.Body)
         module.imports.Funcs = append(module.imports.Funcs, funcs)
         funcs++
      }
   }

这里就跳过了对env中Println的解析,注意这里需要添加code,否则会导致空指令的执行,这里的41表示call指令,我们需要在函数执行call时对自己的函数进行处理。 还有就是我们添加了两个变量,IsEnv和Name,这两个变量可以判断函数是否是我们自己实现的,从而实现外部函数的分支调用。

下一步就是创建新的VM了,创建新的VM时需要在函数列表中添加上我们加入的两个变量:

    vm.funcs[i] = compiledFunction{
      code:           code,
      branchTables:   table,
      maxDepth:       disassembly.MaxDepth,
      totalLocalVars: totalLocalVars,
      args:           len(fn.Sig.ParamTypes),
      returns:        len(fn.Sig.ReturnTypes) != 0,
      IsEnv:          fn.IsEnv,
      Name:           fn.Name,
   }
}

然后函数执行时就会调用到我们放入的code:

func (vm *VM) ExecCode(fnIndex int64, args ...uint64) (rtrn interface{}, err error) {
   compiled, ok := vm.funcs[fnIndex].(compiledFunction)
   if !ok {
      panic(fmt.Sprintf("exec: function at index %d is not a compiled function", fnIndex))
   }
   if len(vm.ctx.stack) < compiled.maxDepth {
      vm.ctx.stack = make([]uint64, 0, compiled.maxDepth)
   }
   vm.ctx.locals = make([]uint64, compiled.totalLocalVars)
   vm.ctx.pc = 0
   vm.ctx.code = compiled.code
   vm.ctx.curFunc = fnIndex

   for i, arg := range args {
      vm.ctx.locals[i] = arg
   }

   res := vm.execCode(compiled)

code里是0x41和0x2b,执行VM的函数调用:

func (vm *VM) execCode(compiled compiledFunction) uint64 {
outer:
   for int(vm.ctx.pc) < len(vm.ctx.code) {
      op := vm.ctx.code[vm.ctx.pc]
      vm.ctx.pc++
      switch op {
      default:
         fmt.Printf("Execte Code:0x%02x\n", op)
         vm.funcTable[op]()
      }
   }

funcTable是在创建VM时设置的,实际调用的是vm的call函数:

func (vm *VM) call() {
   index := vm.fetchUint32()
   fun, ok := vm.funcs[index].(compiledFunction)
   if ok {
      if fun.IsEnv {
         if fun.Name == "Println" {
            fmt.Println("Println Call log aba")
            vm.popUint64()
            vm.pushUint64(0)
            return
         }
      }
   }
   vm.funcs[index].call(vm, int64(index))
}

我们可以在这个函数中做分支,实现自己的API方法,当然,现在的Println函数并没有对堆栈进行处理,只是最简单的流程,如果涉及到堆栈处理,就更复杂一些了。

Tags: