1. (一)初步认识EVM字节码
  2. (二)状态变量的赋值
  3. (三)函数调用

前言

在开始之前,我们默认读者已经初步理解 EVM, 包括字节码、操作码、堆栈、内存、存储、calldata。理解 ABI,能够根据文档计算对应变量的 ABI,并且具有一定的编译基础。因此,如果阅读过程中觉得困难,请先阅读 EVM 的其他文章,如 初步理解以太坊虚拟机

本文编译器版本采用 0.8.10,EVM 版本是 London。没有特殊说明的条件下,默认关闭编译优化。建议读者复现时采用相同的编译器和 EVM,避免不一样的结果,虽然一般而言小版本的变化区别不大。

合约创建

字节码和运行时字节码

我们首先部署一个空的合约 Empty.sol,观察合约部署的字节码。这里需要注意区分运行时代码和部署时代码,creation bytecode 在部署后被舍弃,RETURN 的运行时字节码写入区块链。

1
2
3
4
5
6
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;
contract Empty {

}

在 Remix 的部署页面的最下面有编译细节,里面有辅助的部署函数和部署时、运行时的字节码。

部署时的字节码常直接称作 bytecode:

1
2
"object": "6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220b3cdd68a9a1040f3ba42bb4f6ac7a5ea4dd3119af7649144cd611e3fef9a611564736f6c634300080d0033",
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x3F DUP1 PUSH1 0x1D PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 0xB3 0xCD 0xD6 DUP11 SWAP11 LT BLOCKHASH RETURN 0xBA TIMESTAMP 0xBB 0x4F PUSH11 0xC7A5EA4DD3119AF7649144 0xCD PUSH2 0x1E3F 0xEF SWAP11 PUSH2 0x1564 PUSH20 0x6F6C634300080D00330000000000000000000000 ",

我们开始单步调试,具体过程如果不熟悉的话,请阅读 Remix-ide doc,操作码详解可以见 https://www.evm.codes/

  1. PUSH1 0x80 PUSH1 0x40 MSTORE

偏移 40 字节,0x80 拓展成 256 位,然后写入内存。

按照内存的布局:

  • 0x00 - 0x3f (前面 64 字节,占用 2 个 slot): 计算哈希时临时存储数据的空间,在语句之间使用。
  • 0x40 - 0x5f (32 字节,占用 1 个 slot): 当前分配的内存大小 ,或者说是内存指针所在位置(因为可以通过内存空间大小计算内存指针位置)。
  • 0x60 - 0x7f (32 字节,占用 1 个 slot): slot[0],正式内存,用于保存动态 memory 数组的初始值,而且只读。然后下一个位置 0x80 是开始写入的位置。

我们可以知道,0x80 作为了初始的内存指针。

  1. CALLVALUE DUP1 ISZERO

判断部署合约时给合约的转账金额是否为 0

  1. PUSH1 0xF JUMPI JUMPDEST

0xF 是跳转的位置,如果部署合约的 callvalue 为 0,那么 pc 条件跳转到栈 0xF 的位置,也就是下一个最近的 JUMPDEST 的位置。

  1. POP PUSH1 0x3F DUP1 PUSH1 0x1D PUSH1 0x0 CODECOPY

POP 后清空了栈里最后一个元素,然后栈的内容如下:

1
[ "0x0000000000000000000000000000000000000000000000000000000000000000", "0x000000000000000000000000000000000000000000000000000000000000001d", "0x000000000000000000000000000000000000000000000000000000000000003f", "0x000000000000000000000000000000000000000000000000000000000000003f" ]

最后的 CODECOPY 将会在内存偏移 0 的位置,将字节码偏移 0x1d 后的 3f 个字节复制到内存,直接覆盖原来的值。这里可以知道,存放在临时存储空间。

  1. PUSH1 0x0 RETURN

RETURN 前的栈,最后在内存中偏移量为 0x0 处开始的 3f 个字节写入区块链。

1
[ "0x0000000000000000000000000000000000000000000000000000000000000000", "0x000000000000000000000000000000000000000000000000000000000000003f" ]

所以 RETURN 的值是,这就是运行时字节码

1
6080604052600080fdfea26469706673582212201249c699c4827fdd0ee29a1e00afff56e54b23a7995fd367cf89d5f34b9922df64736f6c634300080a0033

我们再对照之前的部署时字节码,

1
6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220b3cdd68a9a1040f3ba42bb4f6ac7a5ea4dd3119af7649144cd611e3fef9a611564736f6c634300080d0033

可以发现,有许多的差别,省去了不必要的情况,如部署时的转账不为 0.

EVM 汇编

Remix 产生的 EVM 汇编代码:

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
26
27
28
29
30
31
32
.code
PUSH 80 contract Empty{\r\n \r\n}
PUSH 40 contract Empty{\r\n \r\n}
MSTORE contract Empty{\r\n \r\n}
CALLVALUE contract Empty{\r\n \r\n}
DUP1 contract Empty{\r\n \r\n}
ISZERO contract Empty{\r\n \r\n}
PUSH [tag] 1 contract Empty{\r\n \r\n}
JUMPI contract Empty{\r\n \r\n}
PUSH 0 contract Empty{\r\n \r\n}
DUP1 contract Empty{\r\n \r\n}
REVERT contract Empty{\r\n \r\n}
tag 1 contract Empty{\r\n \r\n}
JUMPDEST contract Empty{\r\n \r\n}
POP contract Empty{\r\n \r\n}
PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000 contract Empty{\r\n \r\n}
DUP1 contract Empty{\r\n \r\n}
PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 contract Empty{\r\n \r\n}
PUSH 0 contract Empty{\r\n \r\n}
CODECOPY contract Empty{\r\n \r\n}
PUSH 0 contract Empty{\r\n \r\n}
RETURN contract Empty{\r\n \r\n}
.data
0:
.code
PUSH 80 contract Empty{\r\n \r\n}
PUSH 40 contract Empty{\r\n \r\n}
MSTORE contract Empty{\r\n \r\n}
PUSH 0 contract Empty{\r\n \r\n}
DUP1 contract Empty{\r\n \r\n}
REVERT contract Empty{\r\n \r\n}
.data

简单介绍,.code 包括了合约初始化的字节码,执行完之后就会舍弃。.data 开始是运行时的字节码,每个 tag 是基本块,里面是连续执行的指令,通常顺序执行或者跳转到不同的 tag,例如 PUSH [tag] 1 表示跳转到 tag1 的部分。

solc 产生的 EVM 汇编和 solcjs 的 (如 remix 应该采用 solcjs) 汇编有些差异

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
26
27
28
29
30
31
32
======= Empty.sol:Empty =======
EVM assembly:
/* "Empty.sol":69:91 contract Empty{... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop

sub_0: assembly {
/* "Empty.sol":69:91 contract Empty{... */
mstore(0x40, 0x80)
0x00
dup1
revert

auxdata: 0xa2646970667358221220b4acf947b85370aec1c3e21a1f682830785f96fd0cbcd09512abfffcd7f9e7be64736f6c634300080a0033
}

auxdata 是元数据,是字节码的标识,用的很少。对于压栈操作,直接省略了 push 操作码,给出了数值。

之前提到的都是 callvalue 的值为 0 的情况,现在从汇编看跳转关系可以知道,如果 callvalue 不为 0,那么将会在 sub_0REVERT,说明异常终止,这是因为合约中默认的构造函数是 constructor(){},因此我们需要指定 payable

我们来看构造函数具有 payable 的情况

1
2
3
4
5
6
7
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;
contract Empty{
constructor() payable{
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
======= Empty.sol:Empty =======
EVM assembly:
/* "Empty.sol":69:119 contract Empty{... */
mstore(0x40, 0x80)
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop

sub_0: assembly {
/* "Empty.sol":69:119 contract Empty{... */
mstore(0x40, 0x80)
0x00
dup1
revert

auxdata: 0xa26469706673582212207c6dad3e26954a2823e1b580d4e9cb6c0365802bbf4dbd146e093af53efe9beb64736f6c634300080a0033
}

可以看到,汇编中去除了 ISZERO 的判断,因为 callvalue 可以不为 0。

calldata

部署合约时的 calldata 即合约的字节码。

案例分析

案例分析和总结的章节,大部分来自 https://mp.weixin.qq.com/s/UWU1nuZaGOM0_IHd5AhyvA ,当初朋友这篇文章还没发出来,只是共享了笔记给我,然后我保存了,半年后就忘了,误以为自己写的。在此道歉。也感谢他的帮助和分享。

以一个很简单的合约代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;
contract Foo {

uint x;
constructor(uint _x) {
x = _x;
}

function foo() public view returns (uint) {
return x;
}
}

使用 solc 编译,并打印 evm 汇编

1
solc --asm --optimize test.sol > test.asm

注:--optimize 表示对 evm bytecode 进行优化,从而可以生成较为精简的汇编代码。

生成如下,代码的作用见注释:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
======= test.sol:Foo =======
EVM assembly:
/* "test.sol":62:656 contract Foo {... */
// solidity 将前 0x80 字节的内存用作特殊用途。
// 普通的临时变量等将分配在 0x80 后。
// 0x40 位置为 free memory pointer
// 一般智能合约前几条指令都会进行此设置
// 参考:https://docs.soliditylang.org/en/v0.8.10/internals/layout_in_memory.html
mstore(0x40, 0x80)
// 这里汇编代码实际有一定的简写
// EVM 是栈式虚拟机,实际的指令应该是
// 00000: PUSH1 0x80
// 00002: PUSH1 0x40
// 00004: MSTORE
// 这也是智能合约开头字节大都是 0x6080604052 的原因


// 以下是 constructor 代码。
// 如果没有 constructor 将直接跳到最后的 codecopy 代码。

/* "test.sol":152:259 constructor(uint _x) {... */
callvalue // 交易的 ether 数量
dup1
iszero // 判定是否为 0
tag_1
jumpi // 如果是则跳转
0x00
dup1
revert
// 不是则这里会触发 revert,因为合约中的 constructor 没有标识 payable


tag_1:
pop
mload(0x40)

sub(codesize, bytecodeSize)
// codesize 就是 codesize 指令,会取到执行时代码的总体长度。
// bytecodeSize 实际是编译器生成的立刻数,编译生成出来的代码总长度。
// 二者相减,就是构造函数 ABI 编码后数据的长度。

dup1
bytecodeSize
dup4
codecopy
// 从代码的 bytecodeSize 偏移处,copy 长度 codesize-bytecodeSize 的数据到内存中。
// 实际就是将 constructor 的参数数据放到内存中。

dup2
add
0x40
dup2
swap1
mstore
tag_2
swap2
tag_3
jump // in
// 这里连续压了两个 tag,先跳 tag_3,在 tag_3 执行结束后跳到 tag_2
// 可以理解成函数调用。可将 tag_3 当成一个函数,执行完返回这里继续执行。
tag_2:
// tag_3 已经将 constructor 的参数压在了栈顶
// 这里可以开始执行 constructor 的代码了。
/* "test.sol":183:184 x */
0x00
/* "test.sol":183:189 x = _x */
sstore
/* "test.sol":62:656 contract Foo {... */

jump(tag_7) // constructor 执行完成。跳去 tag_7。
/* "#utility.yul":14:198 */

tag_3: // 这段代码的作用就是解析 ABI 编译的 constructor 的参数
/* "#utility.yul":84:90 */
0x00
/* "#utility.yul":137:139 */
0x20
/* "#utility.yul":125:134 */
dup3
/* "#utility.yul":116:123 */
dup5
/* "#utility.yul":112:135 */
sub
/* "#utility.yul":108:140 */
slt
/* "#utility.yul":105:157 */
iszero // 检查参数数据的长度是否小于 0x20
// 例子的构造函数参数 uint256 正好是这个大小。
tag_9
jumpi // 不小于的情况就跳到 tag_9 去解析数据

// 小于的话说明参数传递有问题,revert.
/* "#utility.yul":153:154 */
0x00
/* "#utility.yul":150:151 */
dup1
/* "#utility.yul":143:155 */
revert
/* "#utility.yul":105:157 */


tag_9:
pop // 这里就是将 memory 中的 ABI 编码数据解码出来压到栈上。
// 这里就 1 个 uint256 参数,直接取出放在栈顶
/* "#utility.yul":176:192 */
mload
swap2
/* "#utility.yul":14:198 */
swap1
pop
jump // out // tag_3 这个“函数” return(即跳到 tag_2)

// constructor 代码结束。


tag_7: // 将代码中 sub_0 copy 到内存中,返回。
/* "test.sol":62:656 contract Foo {... */
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return // EVM 执行结束
// 返回的代码就是合约部署后的代码,相当于 solc --bin-runtime

stop


// 以下是合约主体的代码。
// 在合约部署时,这部分代码完全不会执行到,只是当作纯数据处理。

// 部署后,有合约调用交易时,会执行这部分代码。
sub_0: assembly {
/* "test.sol":62:656 contract Foo {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1 // 由于合约中没有 receive 或者 fallback 函数
// 这里判断转账金额不为 0
// 就会直接 revert
jumpi
0x00
dup1
revert

tag_1: // 解析 calldata,取 selector
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0xc2985578 // 如果是这个值,则跳 tag_3
eq
tag_3
jumpi


tag_2: // 其他情况说明这个交易在尝试调用不存在的函数,revert
0x00
dup1
revert


// tag_3 就是上面合约的 foo 代码。
/* "test.sol":265:332 function foo() public view returns (uint) {... */
tag_3:
/* "test.sol":301:305 uint */
0x00
/* "test.sol":324:325 x */
sload // 取 x 变量(slot 0)
/* "test.sol":265:332 function foo() public view returns (uint) {... */
mload(0x40)
/* "#utility.yul":160:185 */
swap1
dup2
mstore // 放到内存中
/* "#utility.yul":148:150 */
0x20
/* "#utility.yul":133:151 */
add
/* "test.sol":265:332 function foo() public view returns (uint) {... */
mload(0x40)
dup1
swap2
sub
swap1
return // return

// 合约的一些 meta data 参考:https://docs.soliditylang.org/en/v0.8.10/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
auxdata: 0xa26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c63430008090033
}

完整的字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
➜  test solc --optimize --opcodes test.sol

======= test.sol:Foo =======
Opcodes:
// 构造函数及部署代码
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 MLOAD PUSH2 0xD6 CODESIZE SUB DUP1 PUSH2 0xD6 DUP4 CODECOPY DUP2 ADD PUSH1 0x40 DUP2 SWAP1 MSTORE PUSH2 0x2F SWAP2 PUSH2 0x37 JUMP JUMPDEST PUSH1 0x0 SSTORE PUSH2 0x50 JUMP JUMPDEST PUSH1 0x0 PUSH1 0x20 DUP3 DUP5 SUB SLT ISZERO PUSH2 0x49 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP MLOAD SWAP2 SWAP1 POP JUMP JUMPDEST PUSH1 0x78 DUP1 PUSH2 0x5E PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID

// 部署后的代码
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH1 0x28 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0xC2985578 EQ PUSH1 0x2D JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x0 SLOAD PUSH1 0x40 MLOAD SWAP1 DUP2 MSTORE PUSH1 0x20 ADD PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN INVALID

// auxdata,字节码的元标识
LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 SUB CALLDATACOPY 0xA6 0xD0 0xF8 0xE1 0x2D 0x27 LOG3 PUSH27 0x26E2BECB7FE90EE8E3D86A8970E157D9CB79B1A7FE2B64736F6C63 NUMBER STOP ADDMOD MULMOD STOP CALLER

使用下面的命令可以打印出可读性更好的中间语言代码(但里面有许多 solidity 自定义函数的层层封装,会显得比较多),其与汇编的逻辑是一致的,也可供参考。

1
2
solc --ir test.sol
solc --ir-optimized test.sol

下面命令可以打印合约 storage 的布局。

1
2
3
4
5
➜  test solc --storage-layout test.sol

======= test.sol:Foo =======
Contract Storage Layout:
{"storage":[{"astId":3,"contract":"test.sol:Foo","label":"x","offset":0,"slot":"0","type":"t_uint256"}],"types":{"t_uint256":{"encoding":"inplace","label":"uint256","numberOfBytes":"32"}}}

总结

通过上面的分析,可以更透彻的理解智能合约部署和调用的底层逻辑。下面是一些总结。

合约部署

从用户角度看,合约部署是向零地址地址发送合约部署代码,注意零地址不是 0x00.. 。以太坊角度看, data 作为智能合约代码执行,并将输出结果作为合约代码保存在合约地址上。

以案例分析中的合约为例,可以用 evm 验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 生成合约的部署代码
$ solc --bin --optimize test.sol

======= test.sol:Foo =======
Binary:
608060405234801561001057600080fd5b506040516100d63803806100d683398101604081905261002f91610037565b600055610050565b60006020828403121561004957600080fd5b5051919050565b60788061005e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60005460405190815260200160405180910390f3fea26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c63430008090033


# 在上述输出后添加上 '00'*0x20 作为 constructor 的参数。
# 注意这个参数要在 code 的尾部,而不能通过 --input 传递。
$ evm run --code 608060405234801561001057600080fd5b506040516100d63803806100d683398101604081905261002f91610037565b600055610050565b60006020828403121561004957600080fd5b5051919050565b60788061005e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60005460405190815260200160405180910390f3fea26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c634300080900330000000000000000000000000000000000000000000000000000000000000000
# 输出如下:
0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60005460405190815260200160405180910390f3fea26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c63430008090033

# 对比可以发现,前面输出确实就是合约的部署后的代码。
$ test solc --bin-runtime --optimize test.sol

======= test.sol:Foo =======
Binary of the runtime part:
6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60005460405190815260200160405180910390f3fea26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c63430008090033

可以得到如下结论:

  • 智能合约的 constructor 代码,是在部署时执行的。而执行没结束前,无法取得输出,也就向目标地址部署代码。因此 constructor 执行时,合约地址的 codesize 是 0。这就是许多文章都提到,用 codesize 判断某个地址是不是合约的方法可能存在误判的原因。

  • 向地址发送 data 是少有的可以直接执行任意 EVM 代码的地方。正常情况下:

    • 向普通账户发送 data,只当作附加信息处理。
    • 向合约账户发送 data,会当作 input 处理。
  • 以太坊 RPC 接口 eth_call 可在不上链的情况下执行一笔交易。利用这个接口,向 0 地址发送 EVM 代码即可执行任意的 EVM 代码。某些项目没有注意到这一点,被黑客绕过了检查。

如下是利用 eth.call 方法可以执行任意 VM 代码:

1
2
3
4
5
6
7
8
9
10
11
12
# 这段代码的作用是返回 0x000000000000000000000000000000ff
$ evm --input 60ff60005260106010f3 disasm
60ff60005260106010f3
00000: PUSH1 0xff
00002: PUSH1 0x00
00004: MSTORE
00005: PUSH1 0x10
00007: PUSH1 0x10
00009: RETURN
# 执行效果如下:
$ evm --code 0x60ff60005260106010f3 run
0x000000000000000000000000000000ff

相当于:

1
2
> eth.call({data:"0x60ff60005260106010f3"})
"0x000000000000000000000000000000ff"

合约调用

合约调用的交易将 data 作为 input。合约调用的过程也可以用 evm 模拟。

--code 为前面部署后的 Foo 合约。 --inputfoo() 函数所对应的 selector。

1
2
3
$ evm --code 6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60005460405190815260200160405180910390f3fea26469706673582212200337a6d0f8e12d27a37a26e2becb7fe90ee8e3d86a8970e157d9cb79b1a7fe2b64736f6c63430008090033  --input 0xc2985578 run
0x0000000000000000000000000000000000000000000000000000000000000000
# 返回 x 在 storage 中默认的值 0。

注:

  • 合约调用时,data 作为 EVM 的 input,使用合约自身的代码作为 code。这时调用方不再有执行任意 EVM 代码的机会。
  • 根据 solidity 的约定,将 input 前 4 字节作为 selector,用来决定要调用的函数。在前面的分析可以看出,合约代码中使用了类似 switch case 的形式判断 selector 来决定跳转到哪个位置执行(即调用哪个函数)。具体来说,dispatcher 是首先将合约所有的函数签名按照大小排序,然后匹配则是超过一定数量则二分匹配。而合约只会对 public 函数生成 selector。对于内部函数则不会生成。因此内部函数无论如何是无法调用到的(根据内部函数签名生成一个 selector 去尝试调用显然也是不会成功的。)
  • 对于 EVM 来说其所做的事只是将 data 作为 input 执行 code 而已。这些 ABI 的约定其实完全是 solidity 编译器所决定的。如果开发一个私人的编译器,生成的代码以其他形式处理参数序列化的形式,函数选择的方式,也是可以的。
  • payable, receive, fallback 这类的语义都是在 solidity 层面才有的,对于 EVM 来说并不存在专门对应的指令。以 payable 为例,某个函数是不是 payable 的,是 solidity 专门生成了 EVM 指令来判断,callvalue 不为 0 时,如果不主动 revert 则相当于是 payable 的。

参考

附录 I 工具介绍

为了方便读者深入学习,这里简短介绍字节码分析的相关工具。

**一、**Remix IDE 请自行阅读官方文档。

二、go-ethereum 项目官网有工具介绍,这里会较常用字节码调试工具 evm。下面是参数介绍

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
evm [global options] command [command options] [arguments...]

VERSION:
1.10.17-stable-25c9b49f

COMMANDS:
compile compiles easm source to evm binary //evm 抽象语法树,已经废弃
disasm disassembles evm binary //字节码生成操作码
run run arbitrary evm binary //运行字节码,预期的下一个参数是一串16进制字节码,而不是文件
statetest executes the given state tests //不清楚它检查状态输入是什么
transition executes a full state transition //不清楚状态转移的工具是什么
transaction performs transaction validation
block-builder builds a block
help Shows a list of commands or help for one command

GLOBAL OPTIONS:
--bench benchmark the execution //基准测试
--create indicates the action should be create rather than call
--debug output full trace logs //跟踪执行堆栈、存储
--verbosity value sets the verbosity level (default: 0)
--code value EVM code //直接在命令行中以字节码为参数
--codefile value File containing EVM code. If '-' is specified, code is read from stdin //从文件在寻找输入
--gas value gas limit for the evm (default: 10000000000)
--price value price set for the evm (default: 0)
--value value value set for the evm (default: 0)
--dump dumps the state after the run
--input value input for the EVM //部署时的构造函数参数
--inputfile value file containing input for the EVM //从文件中读取构造函数参数
--memprofile value creates a memory profile at the given path
--cpuprofile value creates a CPU profile at the given path
--statdump displays stack and heap memory information
--prestate value JSON file with prestate (genesis) config
--json output trace logs in machine readable format (json)
--sender value The transaction origin
--receiver value The transaction receiver (execution context)
--nomemory disable memory output
--nostack disable stack output
--nostorage disable storage output
--noreturndata enable return data output
--help, -h show help
--version, -v print the version

可以参考我在 stackexchange 中的回答,简单的使用方法就不赘述了。根据官方源码中的介绍,实际上它更新了许多新东西。简单地说是一个调试 evm 的工具,可以指定 fork 分叉,也可以自定义区块高度。然后自定义初始账户在 alloc 里,可以自定义交易在 txt 里,自定义链配置在 env 里,然后输出 storageroot、执行后的账户状态、交易 RLP 编码、堆栈跟踪结果等。

**三、**字节码逆向。我一般使用这两个网站 Online Solidity Decompilerdedaub

image-20220505094351757

逆向后的伪代码的抽象程度较高,能够辅助分析字节码。其中 dedaub 全部用十进制表示数,会让人忽视内在的设计思路,不是很推荐。

**四、**我也尝试过使用 truffle 的调试器,感觉还可以,和 remix ide 差不多。它还推出了 vscode 插件,但是功能不如命令行的好用。感兴趣可阅读插件安装调试教程

**五、**JEB 也有合约逆向工具,主要用法是,打开项目时字节码文件的后缀是 evm-bytecode。进入项目后在左下角找到如下图的这一行,然后右键选择 Decompile。具体可见官方手册。它的类型推断做的不好,但是这也是目前的难点。

image-20220511212202708image-20220511212315319

六、Octopus由于内存布局的方式改变了,已经不能用这,作者还折腾了很久…总之,目前没有找到好用的能够显示字节码数据流图的工具。

**七、**笔者尝试了 IDA, JEB, Binary Ninja 等工具后,发现要么缺少这方面功能,要么很久没维护,过时了。笔者能力足够时,将会自己编写一个显示字节码中数据流图的工具,欢迎感兴趣的朋友一同完成。

附录 II EVM Tracer

a 静态类型的跟踪堆栈

执行的命令为 evm --codefile BYTECODE_FILE --debug --statdump run,代码过长,以 gist 链接附上。

b storage 合约的 sloc 汇编

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
EVM assembly:
/* "function.sol":70:270 contract Storage {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1 //如果栈顶元素是1,那么就跳转
jumpi
0x00
dup1
revert
tag_1:
pop
dataSize(sub_0)//运行时字节码大小,关键逻辑在这里
dup1
dataOffset(sub_0)//写入区块链是,部署时字节码的偏移量
0x00
codecopy
0x00
return
stop

sub_0: assembly { //这是调用函数时的入口
/* "function.sol":70:270 contract Storage {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1//入口
jumpi
0x00
dup1
revert
tag_1:
pop
jumpi(tag_2, lt(calldatasize, 0x04))//如果calldata小于4字节,那么跳转到tag回滚
shr(0xe0, calldataload(0x00))
dup1
0x2e64cec1
eq
tag_3//匹配 retrive签名
jumpi
dup1
0x6057361d
eq
tag_4//匹配store签名
jumpi
tag_2:
0x00
dup1
revert
/* "function.sol":189:268 function retrieve() public view returns (uint256){... */
tag_3:
tag_5
tag_6
jump // in
tag_5:
mload(0x40)
tag_7
swap2
swap1
tag_8
jump // in
tag_7:
mload(0x40)
dup1
swap2
sub
swap1
return
/* "function.sol":119:183 function store(uint256 num) public {... */
tag_4:
tag_9
0x04
dup1
calldatasize
sub
dup2
add
swap1
tag_10
swap2
swap1
tag_11
jump // in
tag_10:
tag_12
jump // in
tag_9:
stop
/* "function.sol":189:268 function retrieve() public view returns (uint256){... */
tag_6:
/* "function.sol":230:237 uint256 */
0x00
/* "function.sol":255:261 number */
dup1
sload
/* "function.sol":248:261 return number */
swap1
pop
/* "function.sol":189:268 function retrieve() public view returns (uint256){... */
swap1
jump // out
/* "function.sol":119:183 function store(uint256 num) public {... */
tag_12:
/* "function.sol":173:176 num */
dup1
/* "function.sol":164:170 number */
0x00
/* "function.sol":164:176 number = num */
dup2
swap1
sstore
pop
/* "function.sol":119:183 function store(uint256 num) public {... */
pop
jump // out
/* "#utility.yul":7:84 */
tag_15:
/* "#utility.yul":44:51 */
0x00
/* "#utility.yul":73:78 */
dup2
/* "#utility.yul":62:78 */
swap1
pop
/* "#utility.yul":7:84 */
swap2
swap1
pop
jump // out
/* "#utility.yul":90:208 */
tag_16:
/* "#utility.yul":177:201 */
tag_25
/* "#utility.yul":195:200 */
dup2
/* "#utility.yul":177:201 */
tag_15
jump // in
tag_25:
/* "#utility.yul":172:175 */
dup3
/* "#utility.yul":165:202 */
mstore
/* "#utility.yul":90:208 */
pop
pop
jump // out
/* "#utility.yul":214:436 */
tag_8:
/* "#utility.yul":307:311 */
0x00
/* "#utility.yul":345:347 */
0x20
/* "#utility.yul":334:343 */
dup3
/* "#utility.yul":330:348 */
add
/* "#utility.yul":322:348 */
swap1
pop
/* "#utility.yul":358:429 */
tag_27
/* "#utility.yul":426:427 */
0x00
/* "#utility.yul":415:424 */
dup4
/* "#utility.yul":411:428 */
add
/* "#utility.yul":402:408 */
dup5
/* "#utility.yul":358:429 */
tag_16
jump // in
tag_27:
/* "#utility.yul":214:436 */
swap3
swap2
pop
pop
jump // out
/* "#utility.yul":523:640 */
tag_18:
/* "#utility.yul":632:633 */
0x00
/* "#utility.yul":629:630 */
dup1
/* "#utility.yul":622:634 */
revert
/* "#utility.yul":769:891 */
tag_20:
/* "#utility.yul":842:866 */
tag_32
/* "#utility.yul":860:865 */
dup2
/* "#utility.yul":842:866 */
tag_15
jump // in
tag_32:
/* "#utility.yul":835:840 */
dup2
/* "#utility.yul":832:867 */
eq
/* "#utility.yul":822:885 */
tag_33
jumpi
/* "#utility.yul":881:882 */
0x00
/* "#utility.yul":878:879 */
dup1
/* "#utility.yul":871:883 */
revert
/* "#utility.yul":822:885 */
tag_33:
/* "#utility.yul":769:891 */
pop
jump // out
/* "#utility.yul":897:1036 */
tag_21:
/* "#utility.yul":943:948 */
0x00
/* "#utility.yul":981:987 */
dup2
/* "#utility.yul":968:988 */
calldataload
/* "#utility.yul":959:988 */
swap1
pop
/* "#utility.yul":997:1030 */
tag_35
/* "#utility.yul":1024:1029 */
dup2
/* "#utility.yul":997:1030 */
tag_20
jump // in
tag_35:
/* "#utility.yul":897:1036 */
swap3
swap2
pop
pop
jump // out
/* "#utility.yul":1042:1371 */
tag_11:
/* "#utility.yul":1101:1107 */
0x00
/* "#utility.yul":1150:1152 */
0x20
/* "#utility.yul":1138:1147 */
dup3
/* "#utility.yul":1129:1136 */
dup5
/* "#utility.yul":1125:1148 */
sub
/* "#utility.yul":1121:1153 */
slt
/* "#utility.yul":1118:1237 */
iszero
tag_37
jumpi
/* "#utility.yul":1156:1235 */
tag_38
tag_18
jump // in
tag_38:
/* "#utility.yul":1118:1237 */
tag_37:
/* "#utility.yul":1276:1277 */
0x00
/* "#utility.yul":1301:1354 */
tag_39
/* "#utility.yul":1346:1353 */
dup5
/* "#utility.yul":1337:1343 */
dup3
/* "#utility.yul":1326:1335 */
dup6
/* "#utility.yul":1322:1344 */
add
/* "#utility.yul":1301:1354 */
tag_21
jump // in
tag_39:
/* "#utility.yul":1291:1354 */
swap2
pop
/* "#utility.yul":1247:1364 */
pop
/* "#utility.yul":1042:1371 */
swap3
swap2
pop
pop
jump // out

auxdata: 0xa26469706673582212202171984ccb0a85eea6b8ddfdd3927496135929d4d68bc35c2a03ee376ced497564736f6c634300080a0033
}