区块链 以太坊 虚拟机 EVM 详解

article/2025/10/30 20:35:19

一、虚拟机

虚拟机用来

  • 执行以太坊上的交易
  • 更改以太坊状态

交易分两种:

  • 普通交易
  • 智能合约交易。

在执行交易时需要支付油费。

智能合约之间的调用有四种方式。

 

二、以太坊虚拟机

以太坊虚拟机,简称 EVM,是用来执行以太坊上的交易的。

业务流程如下图:
业务流程

输入一笔交易,内部会转换成一个 Message 对象,传入 EVM 执行。

如果是一笔普通转账交易,那么直接修改 StateDB 中对应的账户余额即可。

如果是智能合约的创建或者调用,则通过 EVM 中的解释器加载和执行字节码,执行过程中可能会查询或者修改 StateDB。

 

三、固定油费(Intrinsic Gas)

每笔交易过来,不管三七二十一先需要收取一笔固定油费,计算方法如下:
交易油费计算

如果你的交易不带额外数据(Payload),比如普通转账,那么需要收取 21000 的油费。

如果你的交易携带额外数据,那么这部分数据也是需要收费的,具体来说是按字节收费:

  • 字节为 0 的收 4 块,
  • 字节不为 0 收 68 块

所以你会看到很多做合约优化的,目的就是减少数据中不为 0 的字节数量,从而降低油费 gas 消耗。

 

四、生成 Contract 对象

交易会被转换成一个 Message 对象传入 EVM,而 EVM 则会根据 Message 生成一个 Contract 对象以便后续执行:

交易生成对象

可以看到,Contract 中会根据合约地址,从 StateDB 中加载对应的代码,后面就可以送入解释器执行了。

另外,执行合约能够消耗的油费有一个上限,就是节点配置的每个区块能够容纳的 GasLimit

 

五、送入解释器执行

代码跟输入都有了,就可以送入解释器执行了。EVM 是基于栈的虚拟机,解释器中需要操作四大组件:

  • PC:类似于 CPU 中的 PC 寄存器,指向当前执行的指令
  • Stack:执行堆栈,位宽为 256 bits,最大深度为 1024
  • Memory:内存空间
  • Gas:油费池,耗光邮费则交易执行失败

 

解释器四大组件

具体解释执行的流程参见下图:

解释器执行流程

EVM 的每条指令称为一个 OpCode,占用一个字节,所以指令集最多不超过 256,具体描述参见:https://ethervm.io 。

比如下图就是一个示例(PUSH1=0x60, MSTORE=0x52):

OpCode 指令

  • 首先 PC 会从合约代码中读取一个 OpCode,
  • 然后从一个 JumpTable 中检索出对应的 operation,也就是与其相关联的函数集合。
  • 接下来会计算该操作需要消耗的油费,如果油费耗光则执行失败,返回 ErrOutOfGas 错误。
  • 如果油费充足,则调用 execute()执行该指令,根据指令类型的不同,会分别对 Stack、Memory 或者 StateDB 进行读写操作。

 

六、调用合约函数

前面分析完了 EVM 解释执行的主要流程,可能有些同学会问:那么 EVM 怎么知道交易想调用的是合约里的哪个函数呢?别急,前面提到跟合约代码一起送到解释器里的还有一个 Input,而这个 Input 数据是由交易提供的。

Input 数据

Input 数据通常分为两个部分:

  • 前面 4 个字节被称为“4-byte signature”,是某个函数签名的 Keccak 哈希值的前 4 个字节,作为该函数的唯一标识。(可以在该网站查询目前所有的函数签名)

  • 后面跟的就是调用该函数需要提供的参数了,长度不定

举个例子:我在部署完 A 合约后,调用 add(1)对应的 Input 数据是

0x87db03b70000000000000000000000000000000000000000000000000000000000000001

而在我们编译智能合约的时候,编译器会自动在生成的字节码的最前面增加一段函数选择逻辑

  • 首先通过 CALLDATALOAD 指令将“4-byte signature”压入堆栈中,
  • 然后依次跟该合约中包含的函数进行比对,如果匹配则调用 JUMPI 指令跳入该段代码继续执行。

这么讲可能有点抽象,我们可以看一看上图中的合约对应的反汇编代码就一目了然了:

函数 signature

反汇编代码

这里提到了 CALLDATALOAD,就顺便讲一下数据加载相关的指令,一共有 4 种:

  • CALLDATALOAD:把输入数据加载到 Stack 中
  • CALLDATACOPY:把输入数据加载到 Memory 中
  • CODECOPY:把当前合约代码拷贝到 Memory 中
  • EXTCODECOPY:把外部合约代码拷贝到 Memory 中

最后一个 EXTCODECOPY 不太常用,一般是为了审计第三方合约的字节码是否符合规范,消耗的 gas 一般也比较多。

这些指令对应的操作如下图所示:

指令对应的操作

 

 

七、合约调用合约

合约内部调用另外一个合约,有 4 种调用方式:

  • CALL
  • CALLCODE
  • DELEGATECALL
  • STATICALL

后面会专门写篇文章比较它们的异同,这里先以最简单的 CALL 为例,调用流程如下图所示:

CALL 调用流程

可以看到,调用者把调用参数存储在内存中,然后执行 CALL 指令。

CALL 指令执行时会创建新的 Contract 对象,并以内存中的调用参数作为其 Input。

解释器会为新合约的执行创建新的 Stack 和 Memory,从而不会破环原合约的执行环境。

新合约执行完成后,通过 RETURN 指令把执行结果写入之前指定的内存地址,然后原合约继续向后执行。

 

八、创建合约

前面都是讨论的合约调用,那么创建合约的流程时怎么样的呢?

如果某一笔交易的 to 地址为 nil,则表明该交易是用于创建智能合约的。

首先需要创建合约地址,采用下面的计算公式:Keccak(RLP(call_addr, nonce))[:12]

也就是说,对交易发起人的地址和 nonce 进行 RLP 编码,再算出 Keccak 哈希值,取后 20 个字节作为该合约的地址。

下一步就是根据合约地址创建对应的 stateObject,然后存储交易中包含的合约代码

该合约的所有状态变化会存储在一个 storage trie 中,最终以 Key-Value 的形式存储到 StateDB 中。

代码一经存储则无法改变,而 storage trie 中的内容则是可以通过调用合约进行修改的,比如通过 SSTORE 指令。

生成合约地址

 

九、油费计算

最后啰嗦一下油费的计算,计算公式基本上是根据以太坊黄皮书中的定义。
以太坊黄皮书 gas

当然你可以直接 read the fucking code,代码位于 core/vm/gas.go 和 core/vm/gas_table.go 中。

 

十、合约的四种调用方式

在中大型的项目中,我们不可能在一个智能合约中实现所有的功能,而且这样也不利于分工合作。

一般情况下,我们会把代码按功能划分到不同的库或者合约中,然后提供接口互相调用。

在 Solidity 中,如果只是为了代码复用,我们会把公共代码抽出来,部署到一个 library 中,后面就可以像调用 C 库、Java 库一样使用了。

但是 library 中不允许定义任何 storage 类型的变量,这就意味着 library 不能修改合约的状态。

如果需要修改合约状态,我们需要部署一个新的合约,这就涉及到合约调用合约的情况。

合约调用合约有下面 4 种方式:

  • CALL
  • CALLCODE
  • DELEGATECALL
  • STATICCALL

1. CALL vs. CALLCODE

CALL 和 CALLCODE 的区别在于:代码执行的上下文环境不同。

具体来说,CALL 修改的是被调用者的 storage,而 CALLCODE 修改的是调用者的 storage。

storage

我们写个合约验证一下我们的理解:

pragma solidity ^0.4.25;contract A {int public x;function inc_call(address _contractAddress) public {_contractAddress.call(bytes4(keccak256("inc()")));}function inc_callcode(address _contractAddress) public {_contractAddress.callcode(bytes4(keccak256("inc()")));}
}contract B {int public x;function inc() public {x++;}
}

我们先调用一下 inc_call(),然后查询合约 A 和 B 中 x 的值有什么变化:

x 的值

可以发现,合约 B 中的 x 被修改了,而合约 A 中的 x 还等于 0。

我们再调用一下 inc_callcode() 试试:

x 还等于 0

可以发现,这次修改的是合约 A 中 x,合约 B 中的 x 保持不变。

 

2. CALLCODE vs. DELEGATECALL

实际上,可以认为 DELEGATECALL 是 CALLCODE 的一个 bugfix 版本,官方已经不建议使用 CALLCODE 了。

CALLCODE 和 DELEGATECALL 的区别在于:msg.sender 不同。

具体来说,DELEGATECALL 会一直使用原始调用者的地址,而 CALLCODE 不会。

CALLCODE 和 DELEGATECALL 区别

我们还是写一段代码来验证我们的理解:

pragma solidity ^0.4.25;contract A {int public x;function inc_callcode(address _contractAddress) public {_contractAddress.callcode(bytes4(keccak256("inc()")));}function inc_delegatecall(address _contractAddress) public {_contractAddress.delegatecall(bytes4(keccak256("inc()")));}
}contract B {int public x;event senderAddr(address);function inc() public {x++;emit senderAddr(msg.sender);}
}

我们首先调用一下 inc_callcode(),观察一下 log 输出:

log 输出

可以发现,msg.sender 指向合约 A 的地址,而非交易发起者的地址。

我们再调用一下 inc_delegatecall(),观察一下 log 输出:

log 输出

可以发现,msg.sender 指向的是交易的发起者。

 

3. STATICCALL

STATICCALL 放在这里似乎有滥竽充数之嫌,因为目前 Solidity 中并没有一个 low level API 可以直接调用它,仅仅是计划将来在编译器层面把调用 view 和 pure 类型的函数编译成 STATICCALL 指令。

view 类型的函数表明其不能修改状态变量,而 pure 类型的函数则更加严格,连读取状态变量都不允许

目前是在编译阶段来检查这一点的,如果不符合规定则会出现编译错误。如果将来换成 STATICCALL 指令,就可以完全在运行时阶段来保证这一点了,你可能会看到一个执行失败的交易。

话不多说,我们就先看看 STATICCALL 的实现代码吧:

实现代码

可以看到,解释器增加了一个 readOnly 属性,STATICCALL 会把该属性置为 true,如果出现状态变量的写操作,则会返回一个 errWriteProtection 错误。

 

 

 

 

 

 

内容来自https://learnblockchain.cn/2019/04/09/easy-evm


http://chatgpt.dhexx.cn/article/WFvcrLna.shtml

相关文章

以太坊EVM智能合约中的数据存储

目录 EVM基本信息 数据管理 Stack Args Memory Storage 固定长度的值 动态长度数组 Mappings 复杂类型的组合 总结 EVM基本信息 以太坊是一种基于栈的虚拟机,基于栈的虚拟机数据的存取为先进先出,在后面介绍EVM指令的时候会看到这个特性。同时基…

以太坊虚拟机EVM究竟是个啥

基本概念 EVM(ETHereum Virtual Machine)是「以太坊虚拟机」的缩写。如果你有一些软件开发的背景,一定听过java虚拟机。通俗的解释java虚拟机的就是: 我们写的java代码(不只是java,groovy,scala等也可以&a…

介绍 EVM 等效性

介绍 EVM 等效性 上个月,我们宣布了Optimistic 以太坊历史上最重要的升级。最近,我们将Optimistic Kovan迁移到真正的一键式部署,并增加了稳定性,主网在不到三周的时间内就会跟上。 这篇文章是关于我们相信EVM 等效性——完全符合…

EVM的深入研究和分析

最终目标是能够完整地理解已编译的Solidity合同 1、执行 evm-tools 安装 https://github.com/CoinCulture/evm-tools/blob/master/INSTALL.md /home/xue/go/bin/evm --debug --code 366020036101000a600035045b6001900380600c57 --input 05 不同字节码编译成不同EVM指令 1、基…

Ethereum EVM简介

1. 引言 首先需了解以下基本概念: 1)区块链2)世界状态3)账号4)交易5)消息6)去中心化账本7)原子性和顺序性 1.1 何为以太坊区块链? 以太坊可看成是基于交易的状态机&a…

误差向量幅度(EVM)

转自:http://blog.sina.com.cn/s/blog_6c46cb860100otm3.html 误差向量幅度(EVM):误差向量(包括幅度和相位的矢量)是在一个给定时刻理想无误差基准信号与实际发射信号的向量差。Error Vector Magnitude E…

WiFi基础知识

术语和定义 1. 发射功率RF Power 在给定频段范围内发射机通过天线对空间辐射的能量。 2. 矢量误差(EVM) EVM是发射信号理想状态下的IQ分量与实际发送信号的IQ分量之间的矢量差,如图1 所示,其数值等于误差矢量幅度与最大符号幅度之比(取百…

三分钟读懂什么是EVM

虚拟机指的是,通过软件模拟的、具备完整硬件系统功能并运行在隔离环境下的完整计算机系统,比如虚拟化物理机VMware、Java虚拟机等。实际上在PC上常见的各种手机模拟器、主机模拟器,也都是一种虚拟机。这样的虚拟机往往需要固件运行&#xff0…

射频指标之EVM

说到EVM首先先介绍下EVM是什么,其是指目标功率与实际功率的一个矢量差,用下图可以比较详细的表示: 下面就以几个问题来讨论EVM的问题: 1.问题背景: TC芯片输出的EVM正常,单独测试PA输出的EVM也正常&#…

.lib 静态链接库 的破解方法(局限)(1)

因为之前程序调用了一个试用版的.lib库 , 而这个库有时间限制 , 导致程序在试用期过后不能使用 . 然后编译之后要破解一下编译出来的程序 , 很难受 闲来无事,突然想破解一下.lib , 毕竟是个代码库 , 看看是不是破解完了在编译 , 能正常使用 结果当然是能正常用了 不然就没这…

InfluxDB源码编译、安装、配置及主从同步实现

先扯点蛋 公司有个项目要求使用InfluxDB时序数据库储存点东西。第一次听说还有这种数据库,哈哈哈,孤陋寡闻了,先从各位大佬的博客看起,慢慢学习,逐渐了解了之后在服务器上进行安装。直接使用官方包进行安装很简单&…

一种破解静态链接库(.lib)的简单方法

一种破解静态链接库(.lib)的简单方法 作者:游蓝海 博客:http://blog.csdn.net/you_lan_hai 最近,在研究某代码时,遇到这样一个问题:整个解决方案中,有一个工程没有cpp源码,只有头文件跟一个静…

Linux下静态库生成和使用

一.静态库概念 1.库是预编译的目标文件(object files)的集合,它们可以被链接进程序。静态库以后缀为”.a”的特殊的存档(archive file)存储。 2.标准系统库可在目录/usr/lib与/lib中找到。比如,在类Unix系统中C语言的数序库一般…

libuvc介绍及简单使用

libuvc是一个用于USB视频设备的跨平台库,构建在libusb之上,编译libuvc时需要依赖libusb。libuvc的License为BSD,最新发布版本为0.0.6,源码地址: https://github.com/libuvc/libuvc libuvc支持在非windows系统上直接编译&#xff0…

linux下封装函数库——动态库.so和静态库.a(代码实现及链接方式)

在linux环境下的链接库分为静态链接库(.a库)和动态链接库(.so库),其作用是把C程序编译好做成一种可执行链接文件,主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要includ…

Hyperledger fabric应用的多机部署(自动化一键部署)

前面关于fabric部署的介绍都是基于单机环境下的,实际生产环境中一般会根据应用场景将节点分开部署在多台物理机上,面临的难题主要是不同主机间的节点如何通过网络进行通信。文章最后会分享一键完成多机增加组织的自动化部署脚本。 前言 这里仍然以balan…

Linux系统编程makefile制作动态库和静态库

目录 制作动态库 制作静态库 首先准备简单的add.c&#xff0c;sub.c,main.c,head.h.具体代码如下 #head.h文件 int Add(int a, int b); int Sub(int a, int b);#add.c文件 #include <stdio.h> int Add(int a, int b) {return a b; }#sub.c文件 #include <stdio.h&…

Linux动态库的下载与配置(以libevent库为例)

** Linux动态库的下载与配置 ** 本章以下载、安装、配置libevent库为例(安装libevent的前提是已经成功安装openssl库) 一、libevent的下载 https://libevent.org/ //官网下载源码包如果学习下载1.0版本为好&#xff0c;使用可以下载2.0版本 1.如果Linux已经进行桥接联网&am…

Linux下编译安装libusb动态库(.so) - libus1-0 vs libusb-0.1

最近在调试代码的过程中&#xff0c;发现libusb库中的一些方法没办法debug到&#xff0c;所以试着下载了一版源码&#xff0c;编译安装到指定的目录。这样&#xff0c;在工程的pro文件中&#xff0c;直接指定库和头文件的目录就可以引用自己编译的libusb库了。 在网上查了相关的…

linux编译生成动态库、静态库,以及使用

一、介绍 在实际开发过程中&#xff0c;当代码的文件较多&#xff0c;可以将一部分代码编译成动态库或者静态库然后再加载到程序中使用 编译过程 1、预编译 2、编译 3、汇编 4、链接 静态库和动态库的差异 1、链接静态库简单理解就是复制目标代码嵌入可执行文件中 2、动态库是…