EVM架构

EVM具有基于堆栈的体系结构,它将内存中的值都存储在堆栈上,然后使用PC读取堆栈的操作码去执行合约代码。

EVM可以分为三个可寻址组件:

  • Virtual ROM:用于存放代码的虚拟只读存储器,在ROM中的code(智能合约)不能更改。

  • Machine state:相当于RAM,一种易失性的存储器。这里的易失的是指当合约代码执行完毕后,内存的变量会被清除。

  • World state:所有合约的状态变量存储的地方,这部分数据是永久存储在区块链上的数据。

EVM

Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

EVM的字长为256位,这样方便本地哈希和椭圆曲线操作

Machine state

堆栈(stack)

EVM堆栈是用于存储字节码执行过程的中间数据和指令,所有的操作都是在堆栈上执行的。

EVM堆栈的大小是256bit * 1024=28K(字位宽为256,栈深度为1024),堆栈的读写都是以256bit为单位进行的。内联汇编可以使用PUSH、POP、SWAP、DUP指令操作stack。

stack

Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

内存(memory)

EVM内存是线性存储的,可以实现字节级别的寻址。它用于存储临时变量和一些动态大小的数组。solidity可以使用memory来声明内存变量。

内存的字宽是8位。内存一次能读取为256位,而内存一次写入可以为8位或256位。内联汇编能够使用 MSTORE、MSTORE8、MLOAD指令操作memory。

memory

Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

程序计数器(PC)

PC类似于汇编中的寄存器,它指向EVM将要执行的下一条指令。在执行一条指令后,PC通常会增加1 byte(32bit)。其中的例外情况包括JUMP操作码变体,它们将PC重新定位到堆栈顶部指定的位置。

World state

存储(storage)

在以太坊中,每个特定地址的智能合约都有自己的 “存储”,由一个键值存储组成,将 256 位Key映射到 256 位Value。可以通过solidity中定义storage和状态变量去指定变量存储位置。

存储的每次读写都是256bit。内联汇编可以使用SSTORE、SLOAD指令操作storage。

storage

Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

Virtual ROM

合约代码(EVM code)

合约代码是指EVM在本机执行的字节码。在EVM的ROM中只能对合约代码进行读操作。

字节码包含了很多关于合约的信息和逻辑,包括调度器,以及合约元数据。

EVM code

Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

Calldata

根据以太坊黄皮书描述,calldata是一个不限制大小的字节数组,用来指定消息调用的输入数据(这个输入数据是指Massage Call输入的data)。calldata与内存不同,是一段只读的可寻址的保存函数调用参数的空间。

一个消息调用交易包括:

​ data: 一个不限制大小的字节数组,用来指定消息调 用的输入数据,由 Td 表示。

—节选以太坊黄皮书

当一个以太坊合约被外部或EOA调用时,调用者将数据传递给合约,这些数据被存储在calldata区域。在内存或者栈需要使用到该数据时,会通过calldata相关指令集取操作

calldata_6

对calldata操作的指令有三个:

  • calldatasize:返回calldata的大小。
  • calldataload:从calldata中加载32bytes到stack中。
  • calldatacopy:拷贝一些字节到内存中。

calldata_7

Calldata组成

calldata组成由两部分:

  • 前四字节是函数选择器。
  • 其余字节是函数输入参数, 每个输入参数长度为32字节。小于32字节的参数会被填充到32字节长度。
1
2
3
4
5
6
7
8
9
10
11
// a = 1, b = 2
// calldata: 0x04bc52f8(函数选择器)0000000000000000000000000000000000000000000000000000000000000001(a)0000000000000000000000000000000000000000000000000000000000000002(b)
function example(
uint256 a,
uint256 b)
public
pure
returns (bytes memory result)
{
result = abi.encodeWithSignature("foo(uint256,uint256)", a, b);
}

calldata_3

Calldata特性

calldata 区域是只读的,合约不能在执行期间修改它。

读取calldata其实是从calldata区域复制其中的值到堆栈中。

比如:声明一个calldata数组,如果试图修改calldata的参数就会报错。

calldata_1

Messgae Call

在调用者(EOA或合约)发送一个Messgae Call时,会有一个calldata被放入交易的data字段。EVM会为data分配一个只读的空间(大小没有限制) 。EVM能够通过读取calldata中的函数选择器知晓合约中那一个函数的代码能执行。

calldata_2

该图是一个合约之间的Message Call

代码示例

代码逻辑

  • msg.data:可以获得整个calldata数据
  • event Log(bytes data):返回当前交易的calldata
  • 逻辑:用户地址调用合约参数A.example1,输入合约B地址和a,b的值,在example1中会出发Log事件。然后example1会使用call调用B中的add函数,并触发Log事件
  • 预测:根据calldata的原理,第一个触发事件会返回example1的函数选择器和a,b两个参数;第二个触发的事件会返回add的函数选择器和a,b两个参数;其中result和第二个Log中的data值相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract A {

event Log(bytes data);

function example1(address crt,uint256 a, uint256 b) public returns (bytes memory result, bytes memory data) {
emit Log(msg.data);
result = abi.encodeWithSignature("add(uint256,uint256)", a, b);
bool success;
(success, data) = crt.call(result);
}

}

contract B {

event Log(bytes data);

function add(uint256 a, uint256 b) public returns (uint256) {
emit Log(msg.data);
return a + b;
}
}

事件和结果

输入参数:crt=b_addres,a=1,b=5

calldata_4

第一个Log的data前四位字节为:0xebe5f989

第二个Log的data前四位字节为:0x771602f7

返回的result前四位字节为:0x771602f7,且result的值也与data完成相同

引用

1.ethereumbook/13evm.asciidoc at develop · ethereumbook/ethereumbook (github.com)

2.EVM深度分析之数据存储(一)

3.深入了解Solidity数据位置 - Calldata

4.ethereum_yellow_paper_cn.pdf