以太坊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
包含stateDB
、CallContext
两种接口,
在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源码分析学习记录的主要内容,如果未能解决你的问题,请参考以下文章