以太坊虚拟机 EVM(2)Solidity运行原理

article/2025/10/30 16:37:22

作者:储雨知|FISCO BCOS 核心开发者

引 言

作为一门面向智能合约的语言,Solidity与其他经典语言既有差异也有相似之处。

一方面,服务于区块链的属性使其与其他语言存在差异。例如,合约的部署与调用均要经过区块链网络确认执行成本需要被严格控制,以防止恶意代码消耗节点资源。

另一方面,身为编程语言,Solidity的实现并未脱离经典语言,比如Solidity中包含类似栈、堆的设计,采用栈式虚拟机来进行字节码处理

本文将进一步介绍Solidity的内部运行原理,聚焦于Solidity程序的生命周期和EVM工作机制。

Solidity的生命周期

与其他语言一样,Solidity的代码生命周期离不开编译、部署、执行、销毁这四个阶段。

下图整理展现了Solidity程序的完整生命周期:

../../../../_images/IMG_5474.PNG

经编译后,Solidity文件会生成字节码。这是一种类似jvm字节码的代码。

部署时,字节码与构造参数会被构建成交易,这笔交易会被打包到区块中,经由网络共识过程,最后在各区块链节点上构建合约,并将合约地址返还用户。

当用户准备调用该合约上的函数时,调用请求同样也会经历交易、区块、共识的过程,最终在各节点上由EVM虚拟机来执行。

下面是一个示例程序,我们通过remix探索它的生命周期。

pragma solidity ^0.4.25;contract Demo{uint private _state;constructor(uint state){_state = state;}function set(uint state) public {_state = state;}
}

编译

源代码编译完后,可以通过ByteCode按钮得到它的二进制

608060405234801561001057600080fd5b506040516020806100ed83398101806040528101908080519060200190929190505050806000819055505060a4806100496000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b1146044575b600080fd5b348015604f57600080fd5b50606c60048036038101908080359060200190929190505050606e565b005b80600081905550505600a165627a7a723058204ed906444cc4c9aabd183c52b2d486dfc5dea9801260c337185dad20e11f811b0029

还可以得到对应的字节码(OpCode)

PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 MLOAD PUSH1 0x20 DUP1 PUSH2 0xED DUP4 CODECOPY DUP2 ADD DUP1 PUSH1 0x40 MSTORE DUP2 ADD SWAP1 DUP1 DUP1 MLOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP PUSH1 0xA4 DUP1 PUSH2 0x49 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x60FE47B1 EQ PUSH1 0x44 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4F JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x6C PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x6E JUMP JUMPDEST STOP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0x4e 0xd9 MOD DIFFICULTY 0x4c 0xc4 0xc9 0xaa 0xbd XOR EXTCODECOPY MSTORE 0xb2 0xd4 DUP7 0xdf 0xc5 0xde 0xa9 DUP1 SLT PUSH1 0xC3 CALLDATACOPY XOR 0x5d 0xad KECCAK256 0xe1 0x1f DUP2 SHL STOP 0x29 

其中下述指令集为set函数对应的代码,后面会解释set函数如何运行。

JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP

部署

编译完后,即可在remix上对代码进行部署,构造参数传入0x123:

../../../../_images/IMG_5475.PNG

部署成功后,可得到一条交易回执:

../../../../_images/IMG_5476.PNG

点开input,可以看到具体的交易输入数据:

../../../../_images/IMG_5477.PNG

上面这段数据中,

  • 标黄的部分正好是前文中的合约二进制;
  • 标紫的部分,则对应了传入的构造参数0x123。

这些都表明,合约部署以交易作为介质

结合区块链交易知识,我们可以还原出整个部署过程

  • 客户端将部署请求(合约二进制,构造参数)作为交易的输入数据,以此构造出一笔交易
  • 交易经过rlp编码,然后由发送者进行私钥签名
  • 已签名的交易被推送到区块链上的节点
  • 区块链节点验证交易后,存入交易池
  • 轮到该节点出块时,打包交易构建区块,广播给其他节点
  • 其他节点验证区块并取得共识。不同区块链可能采用不同共识算法,FISCO BCOS中采用PBFT取得共识,这要求经历三阶段提交(pre-prepare,prepare, commit)
  • 节点执行交易,结果就是智能合约Demo被创建,状态字段_state的存储空间被分配,并被初始化为0x123

执行

根据是否带有修饰符view,我们可将函数分为两类:调用与交易

  • 由于在编译期就确定了调用不会引起合约状态的变更,故对于这类函数调用,节点直接提供查询即可,无需与其他区块链节点确认
  • 而由于交易可能引起状态变更,故会在网络间确认。

下面将以用户调用了set(0x10)为假设,看看具体的运行过程。

首先,函数set没有配置view/pure修饰符,这意味着其可能更改合约状态

所以这个调用信息会被放入一笔交易,经由交易编码、交易签名、交易推送、交易池缓存、打包出块、网络共识等过程,

最终被交由各节点的EVM执行。

在EVM中,由SSTORE字节码将参数0xa存储到合约字段_state中

该字节码先从栈上拿到状态字段_state的地址与新值0xa,随后完成实际存储。

下图展示了运行过程:

../../../../_images/IMG_5478.PNG

这里仅粗略介绍了set(0xa)是如何运行,下节将进一步展开介绍EVM的工作机制以及数据存储机制。

销毁

由于合约上链后就无法篡改,所以合约生命可持续到底层区块链被彻底关停。若要手动销毁合约,可通过字节码selfdestruct。销毁合约也需要进行交易确认,在此不多作赘述。

EVM原理

在前文中,我们介绍了Solidity程序的运行原理。经过交易确认后,最终由EVM执行字节码。对EVM,上文只是一笔带过,这一节将具体介绍其工作机制。

运行原理

EVM是栈式虚拟机,其核心特征就是所有操作数都会被存储在栈上。

下面我们将通过一段简单的Solidity语句代码看看其运行原理:

uint a = 1;
uint b = 2;
uint c = a + b;

这段代码经过编译后,得到的字节码如下:

PUSH1 0x1
PUSH1 0x2
ADD

为了读者更好了解其概念,这里精简为上述3条语句,但实际的字节码可能更复杂,且会掺杂SWAP和DUP之类的语句。

我们可以看到,在上述代码中,包含两个指令:PUSH1和ADD,它们的含义如下:

  • PUSH1:将数据压入栈顶。
  • ADD:POP两个栈顶元素,将它们相加,并压回栈顶。

这里用半动画的方式解释其执行过程。

下图中,sp表示栈顶指针pc表示程序计数器

当执行完push1 0x1后,pc和sp均往下移:

../../../../_images/IMG_5479.PNG

类似地,执行push1 0x2后,pc和sp状态如下:

../../../../_images/IMG_5480.PNG

最后,当add执行完后,栈顶的两个操作数都被弹出作为add指令的输入,两者的和则会被压入栈:

../../../../_images/IMG_5481.PNG

存储探究

在开发过程中,我们常会遇到令人迷惑的memory修饰符;

阅读开源代码时,也会看到各种直接针对内存进行的assembly操作。不了解存储机制的开发者遇到这些情况就会一头雾水,所以,这节将探究EVM的存储原理。在前文《智能合约编写之Solidity的基础特性》中我们介绍过,一段Solidity代码,通常会涉及到局部变量、合约状态变量。而这些变量的存储方式存在差别,下面代码表明了变量与存储方式之间的关系。

contract Demo{//状态存储uint private _state;function set(uint state) public {//栈存储uint i = 0;//内存存储string memory str = "aaa";}
}

栈用于存储字节码指令的操作数

在Solidity中,局部变量若是整型、定长字节数组等类型,就会随着指令的运行入栈、出栈。

例如,在下面这条简单的语句中,变量值1会被读出,通过PUSH操作压入栈顶:

uint i = 1;

对于这类变量,无法强行改变它们的存储方式,如果在它们之前放置memory修饰符,编译会报错。

内存

内存类似java中的,它用于储存”对象”。

在Solidity编程中,如果一个局部变量属于变长字节数组、字符串、结构体等类型,其通常会被memory修饰符修饰,以表明存储在内存中

本节中,我们将以字符串为例,分析内存如何存储这些对象。

1. 对象存储结构

下面将用assembly语句对复杂对象的存储方式进行分析。

assembly语句用于调用字节码操作

mload指令将被用于对这些字节码进行调用。mload(p)表示从地址p读取32字节的数据。

开发者可将对象变量看作指针直接传入mload。

在下面代码中,经过mload调用,data变量保存了字符串str在内存中的前32字节。

string memory str = "aaa";
bytes32 data;
assembly{data := mload(str)
}  

掌握mload,即可用此分析string变量是如何存储的。

下面的代码将揭示字符串数据的存储方式:

function strStorage() public view returns(bytes32, bytes32){string memory str = "你好";bytes32 data;bytes32 data2;assembly{data := mload(str)data2 := mload(add(str, 0x20))}   return (data, data2);
}

data变量表示str的0~31字节,data2表示str的32~63字节。运行strStorage函数的结果如下:

0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000006
1: bytes32: 0xe4bda0e5a5bd0000000000000000000000000000000000000000000000000000

可以看到,

  • 第一个数据字得到的值为6,正好是字符串”你好”经UTF-8编码后的字节数
  • 第二个数据字则保存的是”你好”本身的UTF-8编码

熟练掌握了字符串的存储格式之后,我们就可以运用assembly修改、拷贝、拼接字符串。

读者可搜索Solidity的字符串库,了解如何实现string的concat。

2. 内存分配方式

既然内存用于存储对象,就必然涉及到内存分配方式。

memory的分配方式非常简单,就是顺序分配

下面我们将分配两个对象,并查看它们的地址:

function memAlloc() public view returns(bytes32, bytes32){string memory str = "aaa";string memory str2 = "bbb";bytes32 p1;bytes32 p2;assembly{p1 := strp2 := str2}   return (p1, p2);
}

运行此函数后,返回结果将包含两个数据字:

0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000080
1: bytes32: 0x00000000000000000000000000000000000000000000000000000000000000c0

这说明,第一个字符串str1的起始地址是0x80,第二个字符串str2的起始地址是0xc0,之间64字节,正好是str1本身占据的空间。

此时的内存布局如下,其中一格表示32字节(一个数据字,EVM采用32字节作为一个数据字,而非4字节):

../../../../_images/IMG_5482.PNG

  • 0x40~0x60:空闲指针,保存可用地址,本例中是0x100,说明新的对象将从0x100处分配。可以用mload(0x40)获取到新对象的分配地址。
  • 0x80~0xc0:对象分配的起始地址。这里分配了字符串aaa
  • 0xc0~0x100:分配了字符串bbb
  • 0x100~…:因为是顺序分配,新的对象将会分配到这里。

状态存储

顾名思义,状态存储用于存储合约的状态字段。

从模型而言,存储由多个32字节的存储槽构成

在前文中,我们介绍了Demo合约的set函数,里面0x0表示的是状态变量_state的存储槽

所有固定长度变量会依序放到这组存储槽中。

对于mapping和数组,存储会更复杂,其自身会占据1槽所包含数据则会按相应规则占据其他槽.

比如mapping中,数据项的存储槽位由键值k、mapping自身槽位p经keccak计算得来。

从实现而言,不同的链可能采用不同实现,

比较经典的是以太坊所采用的MPT树。由于MPT树性能、扩展性等问题,

FISCO BCOS放弃了这一结构,而采用了分布式存储,通过rocksdb或mysql来存储状态数据,使存储的性能、可扩展性得到提高。

结语

本文介绍了Solidity的运行原理,运行原理总结如下。

  1. 首先,Solidity源码会被编译为字节码,部署时,字节码会以交易为载体在网络间确认,并在节点上形成合约;
  2. 合约函数调用,如果是交易类型,会经过网络确认,最终由EVM执行。
  3. EVM是栈式虚拟机,它会读取合约的字节码并执行。
  4. 在执行过程中,会与栈、内存、合约存储进行交互。
  • 其中,栈用于存储普通的局部变量,这些局部变量就是字节码的操作数;
  • 内存用于存储对象,采用length+body进行存储,顺序分配方式进行内存分配;
  • 状态存储用于存储状态变量。

理解Solidity的运行方式及其背后原理,是成为Solidity编程高手必经之路。

https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/articles/3_features/35_contract/solidity_operation_principle.html?highlight=EVM#evm


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

相关文章

IMA/EVM完整性检测代码分析

IMA/EVM完整性检测 IMA(Integrity Measurement Architecture)是一个内核安全子系统,用于检测文件或数据的完整性和安全性。IMA的hook机制指的是内核接口钩子(kernel interface hooks),用于向IMA注册和实现…

为何Cable Loss没补好 EVM会变差

当EVM不好 或是灵敏度不好时 先别急着找硬件问题 先检查Cable Loss 先说结论 先谈谈标题 为何Cable Loss没补好 EVM会变差 多数射频功放输出 会接一个耦合器 将输出功率 耦合到收发器 用意是校正时 侦测输出功率的正确性与否 假设天线头为20dBm Cable loss为5dB 假设天线头…

TI毫米波级联雷达评估板 MMWCAS-DSP-EVM 和MMWCAS-RF-EVM

1. 前言 本文主要是TI的MMWCAS-DSP-EVM 和MMWCAS-RF-EVM 两块评估板的一些使用心得和毫米波雷达的学习总结。 2. 相关原理 毫米波(mmWave)是一类使用短波长电磁波的特殊雷达技术。通过捕捉反射的信号,雷达系统可以确定物体的距离、速度和角度。毫米波雷达可发射波…

DCA1000EVM使用指南

DCA1000EVM使用指南 一、开发环境 1、硬件 AWR1243/xWR1443/xWR1642BOOST(本文以IWR1642BOOST为例)DCA1000EVM5V/2.5A(电流要求不小于2.5A)电源适配器1个或2个 micro USB线2条RJ45网线1根60引脚Samtec连接线(DCA100…

EVM误差矢量幅度

EVM误差矢量幅度是衡量调制精度的一个主要指标,镜像频率的抑制度对EVM的影响取决于镜像频率的抑制度,一般镜像频率抑制度达到31dBc时,对EVM的影响约为5%。镜像频率对信号的影响是因为镜像频率的信号带宽与所需信号的带宽一样,而无…

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

一、虚拟机 虚拟机用来 执行以太坊上的交易,更改以太坊状态。 交易分两种: 普通交易智能合约交易。 在执行交易时需要支付油费。 智能合约之间的调用有四种方式。 二、以太坊虚拟机 以太坊虚拟机,简称 EVM,是用来执行以太坊…

以太坊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…