以太坊EVM源码分析学习记录

Posted isntMahe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以太坊EVM源码分析学习记录相关的知识,希望对你有一定的参考价值。

EVM

待办清单

  • analysis.go
  • common.go
  • contract.go
  • contracts.go
  • doc.go
  • eips.go
  • errors.go
  • evm.go
  • gas.go
  • gas_table.go
  • instructions.go
  • interface.go
  • interpreter.go
  • jump_table.go
  • logger.go
  • memory.go
  • memory_table.go
  • opcodes.go
  • operations_acl.go
  • stack.go
  • stack_table.go

结构与流程

2020年版本的evm结构

大致流程

编写合约 > 生成abi > 解析abi得出指令集 > 指令通过opcode来映射成操作码集 > 生成一个operation[256]

以太坊虚拟机的工作流程:
由solidity语言编写的智能合约,通过编译器编译成bytecode,之后发到以太坊上,以太坊底层通过evm模块支持合约的执行和调用,调用时根据合约获取代码,即合约的字节码,生成环境后载入到 EVM 执行。


opcodes.go

OptionCode(操作码)
OpCode
文件opcodes.go中定义了所有的OpCode,该值是一个byte,合约编译出来的bytecode中,一个OpCode就是上面的一位。opcodes按功能分为9组,以第一位十六进制数来分类,例如0x1x,0x2x。
例如第一组为 算术 操作

// 0x0 range - arithmetic ops.
const (
	STOP       OpCode = 0x0
	ADD        OpCode = 0x1
	MUL        OpCode = 0x2
	SUB        OpCode = 0x3
	DIV        OpCode = 0x4
	SDIV       OpCode = 0x5
	MOD        OpCode = 0x6
	SMOD       OpCode = 0x7
	ADDMOD     OpCode = 0x8
	MULMOD     OpCode = 0x9
	EXP        OpCode = 0xa
	SIGNEXTEND OpCode = 0xb
)

可以使用表格来总结

opCodeRange对应操作
0x0算术操作
0x10比较操作
0x20加密操作
0x30状态闭包
0x40区块操作
0x50存储和执行操作
0x60压栈操作
0x80克隆操作
0x90交换操作
0xa0日志操作
0xf0闭包

实现了判断能否压栈、操作码的byte类型和string类型互相转换的函数或接口。
func StringToOp(str string) OpCode
func (op OpCode) String() string
func (op OpCode) IsPush() bool


AddressLength = 20
HashLength = 32
type Address [AddressLength]byte
type bitvec [ ]byte
// Hash represents the 32 byte Keccak256 hash of arbitrary data.
type Hash [HashLength]byte

contract.go

该文件中包含了饭回合约的调用者信息和value、判断gas值是否足够运行合约执行、

合约的结构

type Contract struct 
	// CallerAddress is the result of the caller which initialised this
	// contract. However when the "call method" is delegated this value
	// needs to be initialised to that of the caller's caller.
	CallerAddress common.Address
	caller        ContractRef
	self          ContractRef

	jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis.
	analysis  bitvec                 // Locally cached result of JUMPDEST analysis

	Code     []byte
	CodeHash common.Hash
	CodeAddr *common.Address
	Input    []byte

	Gas   uint64
	value *big.Int

func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uint64) *Contract

该函数构造了新的合约,且如果是被合约调用,则复用该合约的 jumpdests

func (c *Contract) validJumpdest(dest *uint256.Int) bool
func (c *Contract) isCode(udest uint64) bool

存在两段校验的函数检验代码跳转是否合法以及


  • int类型的大小为 8 字节
  • int8类型大小为 1 字节
  • int16类型大小为 2 字节
  • int32类型大小为 4 字节
  • int64类型大小为 8 字节

analysis.go

func (c *Contract) AsDelegate() *Contract

AsDelegate将合约设置为委托调用并返回当前合同(用于链式调用)


stack.go

为了应对高并发情况下的栈资源问题,代码中创建了 栈池 来保存一些被创造但未使用的栈空间。

var stackPool = sync.Pool
	New: func() interface 
		return &Stackdata: make([]uint256.Int, 0, 16)
	,

除了一些栈该有的基础操作以外,还有:

func (st *Stack) swap(n int) 将从栈顶开始数的第 n 个和栈顶元素交换

func (st *Stack) dup(n int) 复制栈顶元素,并将其压栈

func (st *Stack) Back(n int) *uint256.Int 返回栈底元素


stack_table.go

一些栈的辅助函数


Memory.go

type Memory struct 
	store       []byte
	lastGasCost uint64

为以太坊虚拟机提供一个简单存储的模型

func (m *Memory) Set(offset, size uint64, value []byte) 
func (m *Memory) Set32(offset uint64, val *uint256.Int) 
func (m *Memory) Resize(size uint64)
func (m *Memory) GetCopy(offset, size int64) (cpy []byte)  // 截取切片中的一段 (offset,offset+size)
func (m *Memory) GetPtr(offset, size int64)  // 返回切片中的一段的指针
func (m *Memory) Len() int
func (m *Memory) Data() []byte


Memory_table.go

衡量一些操作所消耗的内存大小同时判断是否会发生栈溢出,如keccak256、callDataCopy、MStore等


EVM.go

EVM机器位宽为256位,即32个字节,256位机器字宽不同于我们经常见到主流的64位的机器字宽

设计256位宽的原因:

  • 时间,智能合约是否能执行得更快
  • 空间,这样是否整体字节码的大小会有所减少gas成本

区块上下文

这里的Random same as difficulty(具体是什么还不知道)

前三个为函数类型,依次作用为 查询转账者账户是否有充足ether支持转账操作转账操作获取第n个区块的hash

其余为一些基础的区块信息,如币基交易地址、Gaslimit、区块高、时间戳、难度值和基础费用

区块一旦创建,区块信息不可以被修改

type BlockContext struct 
	// CanTransfer returns whether the account contains
	// sufficient ether to transfer the value
	CanTransfer CanTransferFunc
	// Transfer transfers ether from one account to the other
	Transfer TransferFunc
	// GetHash returns the hash corresponding to n
	GetHash GetHashFunc

	// Block information
	Coinbase    common.Address // Provides information for COINBASE
	GasLimit    uint64         // Provides information for GASLIMIT
	BlockNumber *big.Int       // Provides information for NUMBER
	Time        *big.Int       // Provides information for TIME
	Difficulty  *big.Int       // Provides information for DIFFICULTY
	BaseFee     *big.Int       // Provides information for BASEFEE
	Random      *common.Hash   // Provides information for PREVRANDAO

交易上下文

Origin是什么,就是第一个交易

type TxContext struct 
	// Message information
	Origin   common.Address // Provides information for ORIGIN
	GasPrice *big.Int       // Provides information for GASPRICE

EVM结构

evm是以太坊虚拟机基础对象,提供工具处理对应上下文中的交易。运行过程中一旦发生错误,状态会回滚并且不退还gas费用,运行中产生的任务错误都会被归结为代码错误。

type EVM struct 
	// Context provides auxiliary blockchain related information
	Context BlockContext
	TxContext
	// StateDB gives access to the underlying state
	StateDB StateDB
	// Depth is the current call stack
	depth int

  // chainconfig是决定区块链设置的核心配置。
  //chainconfig以块为单位存储在数据库中。这意味着
  //任何一个网络,通过它的起源块来识别,都可以有它自己的
  //一组配置选项。
	// 包含了chainId,该链什么时候发生硬分叉,该链难度总和到多少的时候发生更新等信息
	chainConfig *params.ChainConfig
	// chain rules contains the chain rules for the current epoch
	// rules包装了config信息,属于语法糖,是一次性接口,不应
	chainRules params.Rules
	// virtual machine configuration options used to initialise the
	// evm.
	// 解释器的配置信息
	Config Config
	// global (to this context) ethereum virtual machine
	// used throughout the execution of the tx.
	interpreter *EVMInterpreter
	// abort is used to abort the EVM calling operations
	// NOTE: must be set atomically
	// 能够终止evm调用操作
	abort int32
	// callGasTemp holds the gas available for the current call. This is needed because the
	// available gas is calculated in gasCall* according to the 63/64 rule and later
	// applied in opCall*.
	callGasTemp uint64

创建evm,只能用一次

func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM 

reset EVM的交易上下文和状态数据库

func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) 

能够通过原子的修改abort使得取消任何evm操作

func (evm *EVM) Cancel()
func (evm *EVM) Cancelled() bool 

合约预编译的作用

预编译合约是 EVM 中用于提供更复杂库函数(通常用于加密、散列等复杂操作)的一种折衷方法,这些函数不适合编写操作码。 它们适用于简单但经常调用的合约,或逻辑上固定但计算量很大的合约。 预编译合约是在使用节点客户端代码实现的,因为它们不需要 EVM,所以运行速度很快。 与使用直接在 EVM 中运行的函数相比,它对开发人员来说成本也更低。

evm调用深度 <= 1024

evm调用contract的步骤

  • 判断调用深度是否大于1024
  • 判断是否有充足的余额支持调用
  • 进行快照和预编译
  • 检查该地址是否在状态数据库中存在
  • 若不存在,调用一个不存在的帐户,不要做任何事情,只需ping跟踪程序,检查是否是debug模式,若不是则会创建账户
  • 判断是否预编译,若是调用节点客户端代码实现;反之,创建合约对象并加载被调用地址和地址的hash以及代码信息,后用解释器来运行
  • 若运行过程中有任何错误,则状态将会回滚到操作前快照处,并消耗gas

以太坊中的调用call、callcode和delegatecall

调用方式修改的storage调用者的msg.sender被调用者的msg.sender执行的上下文
call被调用者的storage交易发起者的地址调用者的地址被调用者
callcode调用者的storage调用者的地址调用者的地址调用者
delegatecall调用者的storage交易发起者的地址调用者的地址调用者

还有staticCall调用过程中不允许进行任何修改操作,可以用view来修饰,因此在函数实现中会给解释器的运行函数中的read-only参数传入true值。

创建合约

nonce值指定交易数,每发起一笔交易确认后nonce值+1


interpreter.go

解释器中会有一个配置结构体,能够选择debug模式,包含追踪操作码的evm日志,一些eip提议的配置,evm跳表

type Config struct 
	Debug                   bool      // Enables debugging
	Tracer                  EVMLogger // Opcode logger
	NoBaseFee               bool      // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
	EnablePreimageRecording bool      // Enables recording of SHA3/keccak preimages

	JumpTable *JumpTable // EVM instruction table, automatically populated if unset

	ExtraEips []int // Additional EIPS that are to be enabled

范围上下文

解释器结构,包含evm指针,配置信息,hasher??,是否只读,返回数据信息

type EVMInterpreter struct 
	evm *EVM
	cfg Config

	hasher    crypto.KeccakState // Keccak256 hasher instance shared across opcodes
	hasherBuf common.Hash        // Keccak256 hasher result array shared aross opcodes

	readOnly   bool   // Whether to throw on stateful modifications
	returnData []byte // Last CALL's return data for subsequent reuse

func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter

传入evm和配置信息构建新的解释器,根据配置信息设置该链的规则,如遵循eip158、eip150提议。

func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error)

会控制解释器堆栈调用的深度的加减,同时会用传入的合约、和栈池中调用一个栈来创建一个全新的上下文,并为它新建一个memory模型。

run函数中主要部分是处理一个死循环,只会在停止、返回、自毁和出错的时候停止。

通过循环一直执行合约中的操作,并且每次执行之前都要验证堆栈是否在限制范围之中,还要计算由于动态使用空间导致的动态gas费用,检查完这些之后才会由operation来执行操作。


jump_table.go

operation结构体

type operation struct 
	execute     executionFunc
	constantGas uint64
	dynamicGas  gasFunc				
	minStack int							 // 堆栈中需要已有多少项
	maxStack int							 //	堆栈中最多能有多少项(否则执行这个操作的时候会溢出)
	memorySize memorySizeFunc  // 返回该操作需要的内存大小

其中 executionFunc 有四处实现

func makeLog(size int) executionFunc
func makePush(size uint64, pushByteSize int) executionFunc
func makeDup(size int64) executionFunc
func makeSwap(size int64) executionFunc

memorySizeFunc 的实现在memory_table.go文件中

// memory_table.go
func memoryMLoad(stack *Stack) (uint64, bool) 
	return calcMemSize64WithUint(stack.Back(0), 32)


func memoryMStore8(stack *Stack) (uint64, bool) 
	return calcMemSize64WithUint(stack.Back(0), 1)


func memoryMStore(stack *Stack) (uint64, bool) 
	return calcMemSize64WithUint(stack.Back(0), 32)


// common.go
func calcMemSize64WithUint(off *uint256.Int, length64 uint64) (uint64, bool) 

我们可以看到这几个比较熟悉的操作,MLoad、MStore、MStore8 从栈中拿出 偏移量offset地址 + length

查看是否溢出 uint64

Solidity的内存布局将前4个32字节的插槽保留

  • 0x00 - 0x3f (64bytes): 暂存空间(Scratch space)
  • 0x40 - 0x5f (32bytes): 空闲内存指针
  • 0x60 - 0x7f (32bytes): 0 插槽值

他们的作用分别是

  • 用来给hash方法和内联汇编使用
  • 记录当前已经分配的内存大小,空闲内存的起始值为0x80
  • 用作动态内存的初始值,不会被使用

jumpTable包含指向操作的指针

type JumpTable [256]*operation
func validate(jt JumpTable) JumpTable

检查jumpTable中的操作是否为空

我们知道 opsCode是代码的解释器,这里的operation就是opsCode的解释器,interpreter中有一个jumptable,它包含了指向操作的指针,jumptable中的操作就是对应opscode的操作,但是在不同的config配置下,操作集合也会遵循不同的规则。

例如我们可以看看部分代码

// jump_table.go
func newFrontierInstructionSet() JumpTable 
	tbl := JumpTable
		STOP: 
			execute:     opStop,
			constantGas: 0,
			minStack:    minStack(0, 0),
			maxStack:    maxStack(0, 0),
		,
		ADD: 
			execute:     opAdd,
			constantGas: GasFastestStep,
			minStack:    minStack(2, 1),
			maxStack:    maxStack(2, 1),
		,
    ......
    RETURN: 
			execute:    opReturn,
			dynamicGas: gasReturn,
			minStack:   minStack(2, 0),
			maxStack:   maxStack(2, 0),
			memorySize: memoryReturn,
		,
		SELFDESTRUCT: 
			execute:    opSelfdestruct,
			dynamicGas: gasSelfdestruct,
			minStack:   minStack(1, 0),
			maxStack:   maxStack(1, 0),
		,
	
	// Fill all unassigned slots with opUndefined.
  // 将所有没有指定的插槽填充为 未定义操作
	for i, entry := range tbl 
		if entry == nil 
			tbl[i] = &operationexecute: opUndefined, maxStack: maxStack(0, 0)
		
	
	return validate(tbl)


// opscode.go
// 0x0 range - arithmetic ops.
const (
	STOP       OpCode = 0x0
	ADD        OpCode = 0x1
	......
	EXP        OpCode = 0xa
	SIGNEXTEND OpCode = 0xb
)


instructions.go

指令集合,封装了操作指定过程中的堆栈操作。


gas.go

const (
   GasQuickStep   uint64 = 2
   GasFastestStep uint64 = 3
   GasFastStep    uint64 = 5
   GasMidStep     uint64 = 8
   GasSlowStep    uint64 = 10
   GasExtStep     uint64 = 20
)

根据是否遵循EIP150,返回实际的调用产生的费用


gas_table.go

func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error)

一些操作的gas值计算,如自毁、Call、Callcode、delegateCall、staticCall、内存存储等。


logger.go

EVM的日志接口,由不同的tracer或者logger实现。用于从EVM交易执行中收集执行时的信息,能够追踪交易级、调用级、opcode操作码级的信息。


contracts.go

存放预编译好的合约


common.go

存放常用工具方法

func calcMemSize64(off, l *uint256.Int) (uint64, bool) 
func getData(data []byte, start uint64, size uint64) []byte 
func toWordSize(size uint64) uint64
func allZero(b []byte) bool

计算内存空间是否溢出、根据给的参数返回数据切片、 返回内存扩展所需的字的大小、判断是否全0


eips.go

实现了许多eip协议的配置函数,可以通过函数的方式使能跳转表,使其能够遵循某个eip规则。


interface.go

包含stateDBCallContext两种接口,


在evm上,evm字节码是可执行的代码,合约abi是与EVM字节码交互的接口。

ppt中首先介绍web3js , contract abi – json format , evm

contract function 用来支持外部调用,使得应用-合约能够交互,使得合约-合约之间可以联系。

evm bytecode 对应EVM中的一系列的opcode指令

前4个byte是函数名的keccak256的前4个byte 后32byte是十六进制参数 左边用0补齐

所以这个bytecode是4 + 32 = 36 byte


以太坊智能合约虚拟机(EVM)原理与实现

以太坊 EVM原理与实现

以太坊底层通过EVM模块支持合约的执行与调用,调用时根据合约地址获取到代码,生成环境后载入到EVM中运行。通常智能合约的开发流程是用solidlity编写逻辑代码,再通过编译器编译元数据,最后再发布到以太坊上。

技术分享图片

代码结构
.
├── analysis.go            //跳转目标判定
├── common.go
├── contract.go            //合约数据结构
├── contracts.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
│   └── runtime.go          //运行接口 测试使用
├── stack.go                //栈
└── stack_table.go          //栈验证

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

    //算数相关
    const (
        // 0x0 range - arithmetic ops
        STOP OpCode = iota
        ADD
        MUL
        SUB
        DIV
        SDIV
        MOD
        SMOD
        ADDMOD
        MULMOD
        EXP
        SIGNEXTEND
    )

Instruction
文件jump.table.go定义了四种指令集合,每个集合实质上是个256长度的数组,名字翻译过来是(荒地,农庄,拜占庭,君士坦丁堡)估计是对应了EVM的四个发展阶段。指令集向前兼容。

    frontierInstructionSet       = NewFrontierInstructionSet()
    homesteadInstructionSet      = NewHomesteadInstructionSet()
    byzantiumInstructionSet      = NewByzantiumInstructionSet()
    constantinopleInstructionSet = NewConstantinopleInstructionSet()

具体每条指令结构如下,字段意思见注释。

type operation struct {
    //对应的操作函数
    execute executionFunc
    // 操作对应的gas消耗
    gasCost gasFunc
    // 栈深度验证
    validateStack stackValidationFunc
    // 操作所需空间
    memorySize memorySizeFunc

    halts   bool // 运算中止
    jumps   bool // 跳转(for)
    writes  bool // 是否写入
    valid   bool // 操作是否有效
    reverts bool // 出错回滚
    returns bool // 返回
}

按下面的ADD指令为例

定义
    ADD: {
        execute:       opAdd,
        gasCost:       constGasFunc(GasFastestStep),
        validateStack: makeStackFunc(2, 1),
        valid:         true,
    },

操作
不同的操作有所不同,操作对象根据指令不同可能影响栈,内存,statedb。

    func opAdd(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        //弹出一个值,取出一个值(这个值依旧保存在栈上面,运算结束后这个值就改变成结果值)
        x, y := stack.pop(), stack.peek()
        //加运算
        math.U256(y.Add(x, y))
        //数值缓存
        evm.interpreter.intPool.put(x)
        return nil, nil
    }

gas花费
不同的运算有不同的初始值和对应的运算方法,具体的方法都定义在gas_table里面。 按加法的为例,一次加操作固定耗费为3。

    //固定耗费
    func constGasFunc(gas uint64) gasFunc {
        return func(gt params.GasTable, evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
            return gas, nil
        }
    }

除此之外还有两个定义会影响gas的计算,通常作为量化的一个单位。

    //file go-ethereum/core/vm/gas.go
    const (
        GasQuickStep   uint64 = 2
        GasFastestStep uint64 = 3
        GasFastStep    uint64 = 5
        GasMidStep     uint64 = 8
        GasSlowStep    uint64 = 10
        GasExtStep     uint64 = 20

        GasReturn       uint64 = 0
        GasStop         uint64 = 0
        GasContractByte uint64 = 200
    )

    //file go-ethereum/params/gas_table.go
    type GasTable struct {
        ExtcodeSize uint64
        ExtcodeCopy uint64
        Balance     uint64
        SLoad       uint64
        Calls       uint64
        Suicide     uint64

        ExpByte uint64

        // CreateBySuicide occurs when the
        // refunded account is one that does
        // not exist. This logic is similar
        // to call. May be left nil. Nil means
        // not charged.
        CreateBySuicide uint64
    }

memorySize
因为加操作不需要申请内存因而memorySize为默认值0。

栈验证
先验证栈上的操作数够不够,再验证栈是否超出最大限制,加法在这里仅需验证其参数够不够,运算之后栈是要减一的。

    func makeStackFunc(pop, push int) stackValidationFunc {
        return func(stack *Stack) error {
            //深度验证
            if err := stack.require(pop); err != nil {
                return err
            }
            //最大值验证
            //StackLimit       uint64 = 1024 
            if stack.len()+push-pop > int(params.StackLimit) {
                return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit)
            }
            return nil
        }
    }

智能合约
合约是EVM智能合约的存储单位也是解释器执行的基本单位,包含了代码,调用人,所有人,gas相关的信息.

    type Contract struct {
        // CallerAddress is the result of the caller which initialised this
        // contract. However when the "call method" is delegated this value
        // needs to be initialised to that of the caller‘s caller.
        CallerAddress common.Address
        caller        ContractRef
        self          ContractRef

        jumpdests destinations // result of JUMPDEST analysis.

        Code     []byte
        CodeHash common.Hash
        CodeAddr *common.Address
        Input    []byte

        Gas   uint64
        value *big.Int

        Args []byte

        DelegateCall bool
    }

EVM原生预编译了一批合约,定义在contracts.go里面。主要用于加密操作。

// PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum
// contracts used in the Byzantium release.
var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{
    common.BytesToAddress([]byte{1}): &ecrecover{},
    common.BytesToAddress([]byte{2}): &sha256hash{},
    common.BytesToAddress([]byte{3}): &ripemd160hash{},
    common.BytesToAddress([]byte{4}): &dataCopy{},
    common.BytesToAddress([]byte{5}): &bigModExp{},
    common.BytesToAddress([]byte{6}): &bn256Add{},
    common.BytesToAddress([]byte{7}): &bn256ScalarMul{},
    common.BytesToAddress([]byte{8}): &bn256Pairing{},
}

执行机

EVM中栈用于保存操作数,每个操作数的类型是big.int,这就是网上很多人说EVM是256位虚拟机的原因。执行opcode的时候,从上往下弹出操作数,作为操作的参数。

type Stack struct {
    data []*big.Int
}

func (st *Stack) push(d *big.Int) {
    // NOTE push limit (1024) is checked in baseCheck
    //stackItem := new(big.Int).Set(d)
    //st.data = append(st.data, stackItem)
    st.data = append(st.data, d)
}

func (st *Stack) peek() *big.Int {
    return st.data[st.len()-1]
}

func (st *Stack) pop() (ret *big.Int) {
    ret = st.data[len(st.data)-1]
    st.data = st.data[:len(st.data)-1]
    return
}

内存
内存用于一些内存操作(MLOAD,MSTORE,MSTORE8)及合约调用的参数拷贝(CALL,CALLCODE)。

内存数据结构,维护了一个byte数组,MLOAD,MSTORE读取存入的时候都要指定位置及长度才能准确的读写。

    type Memory struct {
        store       []byte
        lastGasCost uint64
    }

    // Set sets offset + size to value
    func (m *Memory) Set(offset, size uint64, value []byte) {
        // length of store may never be less than offset + size.
        // The store should be resized PRIOR to setting the memory
        if size > uint64(len(m.store)) {
            panic("INVALID memory: store empty")
        }

        // It‘s possible the offset is greater than 0 and size equals 0. This is because
        // the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP)
        if size > 0 {
            copy(m.store[offset:offset+size], value)
        }
    }

    func (self *Memory) Get(offset, size int64) (cpy []byte) {
        if size == 0 {
            return nil
        }

        if len(self.store) > int(offset) {
            cpy = make([]byte, size)
            copy(cpy, self.store[offset:offset+size])

            return
        }

        return
    }

内存操作

    func opMload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        offset := stack.pop()
        val := evm.interpreter.intPool.get().SetBytes(memory.Get(offset.Int64(), 32))
        stack.push(val)

        evm.interpreter.intPool.put(offset)
        return nil, nil
    }

    func opMstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        // pop value of the stack
        mStart, val := stack.pop(), stack.pop()
        memory.Set(mStart.Uint64(), 32, math.PaddedBigBytes(val, 32))

        evm.interpreter.intPool.put(mStart, val)
        return nil, nil
    }

    func opMstore8(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
        off, val := stack.pop().Int64(), stack.pop().Int64()
        memory.store[off] = byte(val & 0xff)

        return nil, nil
    }

stateDb
合约本身不保存数据,那么合约的数据是保存在哪里呢?合约及其调用类似于数据库的日志,保存了合约定义以及对他的一系列操作,只要将这些操作执行一遍就能获取当前的结果,但是如果每次都要去执行就太慢了,因而这部分数据是会持久化到stateDb里面的。code中定义了两条指令SSTORE SLOAD用于从db中读写合约当前的状态。

    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
    }

执行过程
执行入口定义在evm.go中,功能就是组装执行环境(代码,执行人关系,参数等)。

    func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
        if evm.vmConfig.NoRecursion && evm.depth > 0 {
            return nil, gas, nil
        }

        // 合约调用深度检查
        if evm.depth > int(params.CallCreateDepth) {
            return nil, gas, ErrDepth
        }
        // balance 检查
        if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
            return nil, gas, ErrInsufficientBalance
        }

        var (
            to       = AccountRef(addr)
            //保存当前状态,如果出错,就回滚到这个状态
            snapshot = evm.StateDB.Snapshot()
        )
        if !evm.StateDB.Exist(addr) {
            //创建调用对象的stateObject
            precompiles := PrecompiledContractsHomestead
            if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
                precompiles = PrecompiledContractsByzantium
            }
            if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
                return nil, gas, nil
            }
            evm.StateDB.CreateAccount(addr)
        }
        //调用别人合约可能需要花钱
        evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

        //创建合约环境
        contract := NewContract(caller, to, value, gas)
        contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

        start := time.Now()

        // Capture the tracer start/end events in debug mode
        if evm.vmConfig.Debug && evm.depth == 0 {
            evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)

            defer func() { // Lazy evaluation of the parameters
                evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
            }()
        }
        //执行操作
        ret, err = run(evm, contract, input)

        // When an error was returned by the EVM or when setting the creation code
        // above we revert to the snapshot and consume any gas remaining. Additionally
        // when we‘re in homestead this also counts for code storage gas errors.
        if err != nil {
            //错误回滚
            evm.StateDB.RevertToSnapshot(snapshot)
            if err != errExecutionReverted {
                contract.UseGas(contract.Gas)
            }
        }
        return ret, contract.Gas, err
    }

类似的函数有四个。详细区别见最后的参考。

Call A->B A,B的环境独立

CallCode、 和Call类似 区别在于storage位置不一样

DelegateCall、 和CallCode类似,区别在于msg.send不一样

StaticCall 和call相似 只是不能修改状态

Contract和参数构造完成后调用执行函数,执行函数会检查调用的是否会之前编译好的原生合约,如果是原生合约则调用原生合约,否则调用解释器执行函数运算合约。

    // run runs the given contract and takes care of running precompiles with a fallback to the byte code interpreter.
    func run(evm *EVM, contract *Contract, input []byte) ([]byte, error) {
        if contract.CodeAddr != nil {
            precompiles := PrecompiledContractsHomestead
            if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
                precompiles = PrecompiledContractsByzantium
            }
            if p := precompiles[*contract.CodeAddr]; p != nil {
                return RunPrecompiledContract(p, input, contract)
            }
        }
        return evm.interpreter.Run(contract, input)
    }

解释器

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

        //返回数据
        in.returnData = nil

        var (
            op    OpCode        // 当前指令
            mem   = NewMemory() // 内存
            stack = newstack()  // 栈
            pc   = uint64(0)    // 指令位置
            cost uint64         // gas花费
            pcCopy  uint64      // debug使用
            gasCopy uint64      // debug使用
            logged  bool        // debug使用
        )
        contract.Input = input  //函数入参

        //*****省略******

        for atomic.LoadInt32(&in.evm.abort) == 0 {
            //获取一条指令及指令对应的操作
            op = contract.GetOp(pc)
            operation := in.cfg.JumpTable[op]
            //valid校验
            if !operation.valid {
                return nil, fmt.Errorf("invalid opcode 0x%x", int(op))
            }
            //栈校验
            if err := operation.validateStack(stack); err != nil {
                return nil, err
            }
            //修改检查
            if err := in.enforceRestrictions(op, operation, stack); err != nil {
                return nil, err
            }

            var memorySize uint64
            //计算内存 按操作所需要的操作数来算
            if operation.memorySize != nil {
                memSize, overflow := bigUint64(operation.memorySize(stack))
                if overflow {
                    return nil, errGasUintOverflow
                }
                // 
                if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
                    return nil, errGasUintOverflow
                }
            }
            // 校验cost 调用前面提到的costfunc 计算本次操作cost消耗
            cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
            if err != nil || !contract.UseGas(cost) {
                return nil, ErrOutOfGas  //超出挂掉
            }
            if memorySize > 0 {
                //如果本次操作需要消耗memory ,扩展memory 
                mem.Resize(memorySize)  
            }

            // 执行操作
            res, err := operation.execute(&pc, in.evm, contract, mem, stack)

            if verifyPool {
                verifyIntegerPool(in.intPool)
            }
            // 如果遇到return 设置返回值
            if operation.returns {
                in.returnData = res
            }

            switch {
            case err != nil:
                return nil, err       //报错
            case operation.reverts:   //出错回滚
                return res, errExecutionReverted
            case operation.halts:
                return res, nil       //停止
            case !operation.jumps:    //跳转
                pc++
            }
        }
        return nil, nil
    }

Solidity案例
和其他语言类似,有了字节码运行机,就可以在字节码上面再组织其他高级语言,而solidlity语言就是实现了这样的语言编译器,方便了合约编写,有利于推广以太坊dapp开发。

pragma solidity ^0.4.17;

contract simple {
      uint num = 0;
    function simple(){
        num = 123;
    }
    
  
    function add(uint i) public returns(uint){
        uint m = 111;
        num =num * i+m;
        return num;
    } 

}

生成的Opcodes码

JUMPDEST 函数入口

PUSH + JUMPI/JUMP 类似于调用函数

CALLDATASIZE + CALLDATALOAD 大约是获取函数参数

.code
  PUSH 80           contract simple {\n      uint ...
  PUSH 40           contract simple {\n      uint ...
  MSTORE            contract simple {\n      uint ...
  PUSH 0            0  //成员变量初始值
  DUP1          uint num = 0
  //从下面这条指令可以看出,初始化的时候成员变量就会存到statedb里面去
  SSTORE            uint num = 0
  CALLVALUE             function simple(){\n        nu...
  DUP1          olidity ^
  ISZERO            a 
  PUSH [tag] 1          a 
  JUMPI             a 
  PUSH 0            r
  DUP1          o
  REVERT            .17;\n
contra
tag 1           a 
  //下面部分是构造函数执行的部分
  JUMPDEST          a 
  POP           function simple(){\n        nu...
  PUSH 7B           123
  PUSH 0            num  
  DUP2          num = 123
  SWAP1             num = 123
  //改变成员变量最后都会写入到statedb里面去
  SSTORE            num = 123
  POP           num = 123
  PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000            contract simple {\n      uint ...
  DUP1          contract simple {\n      uint ...
  PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000         contract simple {\n      uint ...
  PUSH 0            contract simple {\n      uint ...
  CODECOPY          contract simple {\n      uint ...
  PUSH 0            contract simple {\n      uint ...
  RETURN            contract simple {\n      uint ...
  //上面部分做完初始化之后并不会进入到runtime阶段
.data
  0:
    .code
      //下面这段代码大约是处理参数的
      PUSH 80           contract simple {\n      uint ...
      PUSH 40           contract simple {\n      uint ...
      MSTORE            contract simple {\n      uint ...
      PUSH 4            contract simple {\n      uint ...
      CALLDATASIZE          contract simple {\n      uint ...
      LT            contract simple {\n      uint ...
      PUSH [tag] 1          contract simple {\n      uint ...
      JUMPI             contract simple {\n      uint ...
      PUSH 0            contract simple {\n      uint ...
      CALLDATALOAD          contract simple {\n      uint ...
      PUSH 100000000000000000000000000000000000000000000000000000000            contract simple {\n      uint ...
      SWAP1             contract simple {\n      uint ...
      DIV           contract simple {\n      uint ...
      PUSH FFFFFFFF         contract simple {\n      uint ...
      AND           contract simple {\n      uint ...
      DUP1          contract simple {\n      uint ...
      PUSH 1003E2D2         contract simple {\n      uint ...
      EQ            contract simple {\n      uint ...
      PUSH [tag] 2          contract simple {\n      uint ...
      JUMPI             contract simple {\n      uint ...
    tag 1           contract simple {\n      uint ...
      JUMPDEST          contract simple {\n      uint ...
      PUSH 0            contract simple {\n      uint ...
      DUP1          contract simple {\n      uint ...
      REVERT            contract simple {\n      uint ...
    tag 2           function add(uint i) public re...
      JUMPDEST          function add(uint i) public re...
      CALLVALUE             function add(uint i) public re...
      DUP1          olidity ^
      ISZERO            a 
      PUSH [tag] 3          a 
      JUMPI             a 
      PUSH 0            r
      DUP1          o
      REVERT            .17;\n
contra
    tag 3           a 
      JUMPDEST          a 
      POP           function add(uint i) public re...
      PUSH [tag] 4          function add(uint i) public re...
      PUSH 4            function add(uint i) public re...
      DUP1          function add(uint i) public re...
      CALLDATASIZE          function add(uint i) public re...
      SUB           function add(uint i) public re...
      DUP2          function add(uint i) public re...
      ADD           function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      DUP1          function add(uint i) public re...
      DUP1          function add(uint i) public re...
      CALLDATALOAD          function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      PUSH 20           function add(uint i) public re...
      ADD           function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      SWAP3             function add(uint i) public re...
      SWAP2             function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      POP           function add(uint i) public re...
      POP           function add(uint i) public re...
      POP           function add(uint i) public re...
      PUSH [tag] 5          function add(uint i) public re...
      JUMP          function add(uint i) public re...
    tag 4           function add(uint i) public re...
      JUMPDEST          function add(uint i) public re...
      PUSH 40           function add(uint i) public re...
      MLOAD             function add(uint i) public re...
      DUP1          function add(uint i) public re...
      DUP3          function add(uint i) public re...
      DUP2          function add(uint i) public re...
      MSTORE            function add(uint i) public re...
      PUSH 20           function add(uint i) public re...
      ADD           function add(uint i) public re...
      SWAP2             function add(uint i) public re...
      POP           function add(uint i) public re...
      POP           function add(uint i) public re...
      PUSH 40           function add(uint i) public re...
      MLOAD             function add(uint i) public re...
      DUP1          function add(uint i) public re...
      SWAP2             function add(uint i) public re...
      SUB           function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      RETURN            function add(uint i) public re...
    tag 5           function add(uint i) public re...
      //函数内容
JUMPDEST            function add(uint i) public re...
      //这下面就是函数的代码了
      PUSH 0            uint //局部变量在栈里面
      DUP1          uint m
      PUSH 6F           111
      SWAP1             uint m = 111
      POP           uint m = 111 //从push0到这里实现了定义局部变量并赋值
      DUP1          m
      DUP4          i            //获取参数
      PUSH 0            num
      SLOAD             num      //上面那句和这句实现了读取成员变量
      MUL           num * i      //乘
      ADD           num * i+m    //加
      PUSH 0            num
      DUP2          num =num * i+m
      SWAP1             num =num * i+m   //这三句赋值
      SSTORE            num =num * i+m   //成员变量存储
      POP           num =num * i+m
      //下面几句实现return
      PUSH 0            num
      SLOAD             num
      SWAP2             return num    
      POP           return num
      POP           function add(uint i) public re...
      SWAP2             function add(uint i) public re...
      SWAP1             function add(uint i) public re...
      POP           function add(uint i) public re...
      JUMP [out]            function add(uint i) public re...
    .data

技术分享图片

参考

Call、CallCode、DelegateCall:https://ethereum.stackexchange.com/questions/3667/difference-between-call-callcode-and-delegatecall

solidity结构:https://solidity.readthedocs.io/en/develop/structure-of-a-contract.html#

runtime bytecode和bytecode :https://ethereum.stackexchange.com/questions/13086/solc-bin-vs-bin-runtime/13087#13087

remix: https://remix.ethereum.org/

转自:(魂祭心)https://my.oschina.net/hunjixin/blog/1805306















以上是关于以太坊EVM源码分析学习记录的主要内容,如果未能解决你的问题,请参考以下文章

以太坊EVM兼容区块链全表

区块链知识系列 - 系统学习EVM

尚硅谷以太坊区块链学习

以太坊EVM兼容链大全

死磕以太坊源码分析之blockChain分析

笔记区块链ETH学习1--基础概念与钱包使用