Wagon实现一个log函数

[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函数并没有对堆栈进行处理,只是最简单的流程,如果涉及到堆栈处理,就更复杂一些了。

最后更新于