以太坊源码分析之八虚拟机

以太坊源码分析之八虚拟机

一、智能合约的编译分析

接上文,先搞一个小的智能合约,编译好,放到环境里执行看流程和结果,先看智能合约 :

pragma solidity ^0.4.4;

 

contract HelloWorld {

    function Hello( uint256 t) returns (uint256) {

        t = t +1;

        return t;

    }

}最简单的一个HelloWorld,简单就意味着好分析,第一行表示使用的solidity的版本,不同版本编译结果略有不同。^代表 兼容的版本,这里是指兼容0.4.4~0.4.9,当然你也可以指定更新的。

合约的主体结构和普通的编程方式没有太大的区别,使用 contract表示一个封装,你理解成类或者结构体都可以,它也可以有接口有继承。

内部访问有各种限制,公有私有和索引等。

然后将其拷贝到REMIX上,进行编译会发现有两个警告:

browser/ballot_test.sol:4:5: Warning: No visibility specified. Defaulting to "public". 
    function Hello( uint256 t) returns (uint256) {
^ (Relevant source part starts here and spans across multiple lines).
 
browser/ballot_test.sol:4:5: Warning: Function state mutability can be restricted to pure
    function Hello( uint256 t) returns (uint256) {
    ^ (Relevant source part starts here and spans across multiple lines).

这两上警告主要是告诉你的代码写得不规范,没有明确的指定可访问性和限制属性,这两个都可以忽略。将其部署到私链上,调用即可返回“HelloWorld”的字符串。(部署的方法见前面的博客)

看一下它的编译结果:

  //全局初始化

.code
  PUSH 80                      contract HelloWorld {\n    fun...
  PUSH 40                      contract HelloWorld {\n    fun...
  MSTORE                       contract HelloWorld {\n    fun...
  //此处检测返回value,如果该合约构造函数不支持payable,不为0,revert
  CALLVALUE                    contract HelloWorld {\n    fun...
  DUP1                 olidity ^
  ISZERO                       a 
  PUSH [tag] 1                 a 
  JUMPI                        a 
  PUSH 0                       a
  DUP1                 n
  REVERT                       .4;\n
contrac
tag 1                  a     //此处为返回接口代码初始化
  JUMPDEST                     a 
  POP                  contract HelloWorld {\n    fun...
  PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000                contract HelloWorld {\n    fun...
  DUP1                 contract HelloWorld {\n    fun...
  PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000                 contract HelloWorld {\n    fun...
  PUSH 0                       contract HelloWorld {\n    fun...
  CODECOPY                     contract HelloWorld {\n    fun...
  PUSH 0                       contract HelloWorld {\n    fun...
  RETURN                       contract HelloWorld {\n    fun...
.data
  0:       //此处为接口判断代码
    .code
      PUSH 80                  contract HelloWorld {\n    fun...
      PUSH 40                  contract HelloWorld {\n    fun...
      MSTORE                   contract HelloWorld {\n    fun...
      PUSH 4                   contract HelloWorld {\n    fun...
      CALLDATASIZE                    contract HelloWorld {\n    fun...
      LT                       contract HelloWorld {\n    fun...
      PUSH [tag] 1                    contract HelloWorld {\n    fun...
      JUMPI                    contract HelloWorld {\n    fun...
      PUSH FFFFFFFF                   contract HelloWorld {\n    fun...
      PUSH 100000000000000000000000000000000000000000000000000000000                contract HelloWorld {\n    fun...
      PUSH 0                   contract HelloWorld {\n    fun...
      CALLDATALOAD                    contract HelloWorld {\n    fun...
      DIV                      contract HelloWorld {\n    fun...
      AND                      contract HelloWorld {\n    fun...
      PUSH 775A9482                   contract HelloWorld {\n    fun...
      DUP2                     contract HelloWorld {\n    fun...
      EQ                       contract HelloWorld {\n    fun...
      PUSH [tag] 2                    contract HelloWorld {\n    fun...
      JUMPI                    contract HelloWorld {\n    fun...
    tag 1                      contract HelloWorld {\n    fun...
      JUMPDEST                 contract HelloWorld {\n    fun...
      PUSH 0                   contract HelloWorld {\n    fun...
      DUP1                     contract HelloWorld {\n    fun...
      REVERT                   contract HelloWorld {\n    fun...
    tag 2                      function Hello( uint256 t) ret...
      JUMPDEST                 function Hello( uint256 t) ret...
      CALLVALUE                       function Hello( uint256 t) ret...
      DUP1                     olidity ^
      ISZERO                   a 
      PUSH [tag] 3                    a 
      JUMPI                    a 
      PUSH 0                   a
      DUP1                     n
      REVERT                   .4;\n
contrac
    tag 3                      a 
      JUMPDEST                 a 
      POP                      
      PUSH [tag] 4                    function Hello( uint256 t) ret...
      PUSH 4                   function Hello( uint256 t) ret...
      CALLDATALOAD                    function Hello( uint256 t) ret...
      PUSH [tag] 5                    function Hello( uint256 t) ret...
      JUMP                     function Hello( uint256 t) ret...
    tag 4                      function Hello( uint256 t) ret...
      JUMPDEST                 function Hello( uint256 t) ret...
      PUSH 40                  function Hello( uint256 t) ret...
      DUP1                     function Hello( uint256 t) ret...
      MLOAD                    function Hello( uint256 t) ret...
      SWAP2                    function Hello( uint256 t) ret...
      DUP3                     function Hello( uint256 t) ret...
      MSTORE                   function Hello( uint256 t) ret...
      MLOAD                    function Hello( uint256 t) ret...
      SWAP1                    function Hello( uint256 t) ret...
      DUP2                     function Hello( uint256 t) ret...
      SWAP1                    function Hello( uint256 t) ret...
      SUB                      function Hello( uint256 t) ret...
      PUSH 20                  function Hello( uint256 t) ret...
      ADD                      function Hello( uint256 t) ret...
      SWAP1                    function Hello( uint256 t) ret...
      RETURN                   function Hello( uint256 t) ret...
    tag 5                      function Hello( uint256 t) ret...
      JUMPDEST                 function Hello( uint256 t) ret...
      PUSH 1                   1
      ADD                      t +1
      SWAP1                    t +1
      JUMP [out]                      function Hello( uint256 t) ret...
    .data

 

不要被吓到,前头一大堆都是格式化的东西,真正对分析有用处的是tag 5中的部分,明显可以看出指令增加和调换。然后跳转到返回。

看一下它的RUNTIME BYTECODE:

{

    "linkReferences": {},

    "object": "608060405260043610603e5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663775a948281146043575b600080fd5b348015604e57600080fd5b506058600435606a565b60408051918252519081900360200190f35b600101905600a165627a7a72305820c2f8e37dd71a65637998ad372a063dd9df1dfd4214e967a8f469d7605f8855470029",

    "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3E JUMPI PUSH4 0xFFFFFFFF PUSH29 0x100000000000000000000000000000000000000000000000000000000 PUSH1 0x0 CALLDATALOAD DIV AND PUSH4 0x775A9482 DUP2 EQ PUSH1 0x43 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4E JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x58 PUSH1 0x4 CALLDATALOAD PUSH1 0x6A JUMP JUMPDEST PUSH1 0x40 DUP1 MLOAD SWAP2 DUP3 MSTORE MLOAD SWAP1 DUP2 SWAP1 SUB PUSH1 0x20 ADD SWAP1 RETURN JUMPDEST PUSH1 0x1 ADD SWAP1 JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xc2 0xf8 0xe3 PUSH30 0xD71A65637998AD372A063DD9DF1DFD4214E967A8F469D7605F8855470029 ",

    "sourceMap": "25:116:0:-;;;;;;;;;;;;;;;;;;;;;;;51:88;;8:9:-1;5:2;;;30:1;27;20:12;5:2;-1:-1;51:88:0;;;;;;;;;;;;;;;;;;;;;;113:1;110:4;;51:88::o"

}

再看一下部署的CODE:

data: '0x6080604052348015600f57600080fd5b50609c8061001e6000396000f300608060405260043610603e5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663775a948281146043575b600080fd5b348015604e57600080fd5b506058600435606a565b60408051918252519081900360200190f35b600101905600a165627a7a72305820c2f8e37dd71a65637998ad372a063dd9df1dfd4214e967a8f469d7605f8855470029', 

 

不难发现,其实运行时的Code是部署的Code的一子集 。

下面分析一下:

Push 1表示将临时变量压入栈中。

Add  t+1表示将t和1相加并保存结果。

SWAP1  t+1表示将结果交换到t,即将栈顶元素与栈中第1个(SWAPI代表第I个)交换

JUMP [out] 就返回了结果

分析一小段相关的代码,见上面紫色部分:60200190f35b6001019056

先看一上标准操作码:

0x00            STOP

0x01  ADD

0x90            SWAP1

0xf3   RETURN

0x5b  JUMPDEST

0x60            PUSH1

0x56            JUMP

然后和代码对比:

60    01

PUSH 01

01       Add    t+1

90       swap1  t=t+1

56       jump          

再将红色部分与 tag4对比,发现也是一致的。

通过上面的简单分析可以显示出,以太坊的虚拟机是基于栈操作的图灵完备(准确的说是准图灵完备)的虚拟机。

在编译合约时可以得到两个字节码:

contract bytecode(合约字节码):最终存储在区块链中的字节码,也是将字节码存放在区块链、初始化智能合约(运行构造函数)时所需的字节码。

runtime bytecode(运行时字节码):对应于存储在区块链中的字节码,与合约初始化和存放过程无关。

前面已经提到过,后者是前者的一个子集。

而一个个的指令编译出来,就可以按照这个来收gas费用了,举一个例子:

sstore指令第一次写入一个新位置需要花费20000 gas

sstore指令后续写入一个已存在的位置需要花费5000 gas

sload指令的成本是500 gas

大多数的指令成本是3~10 gas

智能合约的存储是收费比较高的,所以一定要注意合理使用变量。

到现在为止,一个虽然简单但是完整的智能合约就编译并分析出来,正好对应着一篇的分析,下面开始分析虚拟机的部分。

二、虚拟机分析

虚拟机部分主要在core/vm中,其它的地方散布着一些调用的接口相关的封装:

 

 analysis.go            //跳转目标判定

 common.go

 contract.go            //合约数据结构

 contracts.go           //预编译好的合约

 doc.go

 errors.go

 evm.go                 //执行器 对外提供一些外部接口  

 gas.go                 //call gas花费计算 一级指令耗费gas级别

 gas_table.go           //指令耗费计算函数表

 gen_structlog.go       //日志数据结构生成

 instructions.go        //指令操作

 interface.go          

 interpreter.go         //解释器 调用核心

 intpool.go             //int值池

 int_pool_verifier_empty.go

 int_pool_verifier.go

 jump_table.go           //指令和指令操作(操作等)对应表

 logger.go               //状态日志

 memory.go               //EVM 内存

 memory_table.go         //EVM 内存操作表 主要判断操作所需内存

 noop.go                 //空指令

 opcodes.go              //Op指令 以及一些对应关系    

 runtime

    env.go              //执行环境

    fuzz.go              Go随机测试工具

    runtime.go          //运行接口 测试使用

 stack.go                //栈

stack_table.go          //栈验证

vm_jit.go              //这两个文件基本被注释空

vm_jit_fake.go

 

它主要分为以下几类:

1、操作码(OpCode)

文件opcodes.go中定义了所有的OpCode,该值是一个byte,智能合约编译出来的bytecode中,一个OpCode就是上面的一位。opcodes按功能分为9组(运算相关,块操作,加密相关等)。

const (

         // 0x40 range - block operations

         BLOCKHASH OpCode = 0x40 + iota

         COINBASE

         TIMESTAMP

         NUMBER

         DIFFICULTY

         GASLIMIT

)

前面已经分析过一个简单的合约。

2、指令和指令集。

jump.table.go定义了三种指令集合,每个集合实质上是个256长度的数组,名字翻译过来是荒地,农庄,拜占庭,指令集向前兼容。

    frontierInstructionSet       = NewFrontierInstructionSet()

    homesteadInstructionSet      = NewHomesteadInstructionSet()

    byzantiumInstructionSet      = NewByzantiumInstructionSet()

下面举一个例子:

                   MUL: {

                            execute:       opMul,

                            gasCost:       constGasFunc(GasFastStep),

                            validateStack: makeStackFunc(2, 1),

                            valid:         true,

                   },

 

3、栈和内存

栈是用来执行指令的一个容器,前面说过。

type Stack struct {

         data []*big.Int

}

 

func newstack() *Stack {

         return &Stack{data: make([]*big.Int, 0, 1024)}

}

 

func (st *Stack) Data() []*big.Int {

         return st.data

}

内存是提供指令数据存储的地方:

// Memory implements a simple memory model for the ethereum virtual machine.

type Memory struct {

         store       []byte

         lastGasCost uint64

}

 

func NewMemory() *Memory {

         return &Memory{}

}

4、状态数据库

状态数据库由stateDb来保存,持久化到内存中,并依据具体的情况保存到 leveldb中去。在指令集的操作函数中:

func opSload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {

         loc := common.BigToHash(stack.pop())

         val := evm.StateDB.GetState(contract.Address(), loc).Big()

         stack.push(val)

         return nil, nil

}

 

func opSstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {

         loc := common.BigToHash(stack.pop())

         val := stack.pop()

         evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(val))

 

         evm.interpreter.intPool.put(val)

         return nil, nil

}

5、入口函数

入口函数在前面提到了, evm.go中,Call,CallCode,StaticCall和DelegateCall,不再赘述。

6、解释执行

前面也提到了,Run函数:

func (in *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err error) {…}

 

 

三、以太坊的EVM说明

以太坊的虚拟机主要是为了执行智能合约而出现,在以太坊的系统中,外部帐户和合约帐户对EVM来说都是一致的。每个帐户都有一个<key,value>来形成持久化存储(仍然在内存中,根据情况可存储到区块上或者日志中),二者的长度均为256位。

同样,帐户在操作内存时,为了简单,是以32字节(256位)为单位的。内存可以在一定范围内线性增长,但是需要消耗gas。

前面分析了,EVM是基于栈而不是常见的计算机的基于寄存器的,说通俗些,其实就是一个后进先出的容器,来回根据指令操作。

EVM的指令集数量比较少,其指令操作的长度恒定为32字节(256位)长度。具备常用的算术、位、逻辑和比较操作。支持有条件和无条件跳转。同时提供合约访问当前区块的相关属性,比如它的编号和时间戳。

最重要的,合约可以通过索引数据结构,将存储的数据映射到区块上,永久保存,这被称为日志logs,Solidity用它来实现事件Events。外总程序可以通过接口,利用布隆过滤器(Bloom Filter),得到想得到的日志数据,而不需要将整个区块同步到本地上。

EVM的编译器并不会为字节码的大小、速度或内存高效性进行优化。它只会为gas的使用进行优化,目的很直接,鼓励计算排序,提高以太坊区块链的执行效率。

四、总结

前面通过一个简单的例子并针对其进行EVM的初步分析,其实一个更好的方式是从智能合约、编译器和EVM虚拟机三个方面进行不同维度的说明,但这就超出了源码的分析范畴了。

更详细的智能合约编写、编译等,将单独开出一个系列来进行,在针对以太坊源码的分析中将不再展开。

如果对区块链和c++感兴趣,欢迎关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值