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

普通函数调用

简单赋值

注意:需要区分部署时和运行时字节码,参考下面这张图。虽然现在的字节码有小改动,但是仍然有参考意义。

image-20220519203646175

所以在编译成字节码是应该使用 solc --bin-runtime FILE_NAME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

uint256 number;

function store(uint256 num) public {
number = num;
}

function retrieve() public view returns (uint256){
return number;
}
}

我们在部署时字节码正常的流程中是看不到函数的调用流程的。虽然 solc 产生的汇编使用 tag,划分了操作码,但是 tag 分的太多,很难阅读。读者可以快速浏览附录 II b中的汇编代码,整体把握。

JEB 的反汇编功能比较强大:但是,JEB 逆向后的代码可能有小错误。

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
contract DecompiledContract {
uint256 g0_0;

function start() {
uint256 v0;

*0x40 = 0x80;
if(msg.value != 0x0) {
revert(0x0, 0x0);
}

v0 = v1.length;
if(v0 >= 0x4) {
if(msg.sig == 778358465) {//retrieve
retrieve();
}
if(msg.sig == 1616328221) {//store
store();
}
}

revert(0x0, 0x0);
}

function retrieve() public payable {
uint256 v0 = sub_75();//slot 0 的内容

v0 = sub_A1(*0x40, v0);//内存大小 0xa0。这里用同一个变量 v0 标识内存大小,有些容易误导
return(*0x40, v0 - *0x40);//返回值是,在内存中偏移量为 0x80 的 0x20 字节大小的数据
}

function store() public payable {
uint256 v0;

v0 = v2.length;//calldatasize,4字节的函数签名,32字节的参数的ABI编码
uint256 v1 = sub_ED(0x4, v0);//返回需要赋值的参数
sub_7E(v1);
stop();//正常结束,无返回值
}

function sub_75() private view returns (uint256) {
uint256 result;

result = storage[0x0];
return result;
}

function sub_7E(uint256 param0) private {
g0_0 = param0;//给变量赋值
}

function sub_88(uint256 param0) private pure returns (uint256) {
return param0;
}

function sub_92(uint256* param0, uint256 param1) private pure {
uint256 v0 = sub_88(param1);//param1

*param0 = v0;//内存0x80开始存放slot0 的内容
}

function sub_A1(uint256 param0, uint256 param1) private pure returns (uint256) {
sub_92(param0, param1);//(内存大小、slot0内容)
//param1=0x80
return param1 + 0x20;//内存结束位置0x80+0xa0
}

function sub_C1(uint256 param0) private pure {
uint256 v0 = sub_88(param0);//参数赋值

if(param0 != v0) {
revert(0x0, 0x0);
}
}

function sub_D8(void param0, uint256 param1) private pure returns (uint256) {//(0x24,0x4)
uint256 result;

result = calldataload(param1);//偏移4字节,也就是跳过函数签名,加载参数。
sub_C1(result);//传入参数
return result;
}

function sub_ED(uint256 param0, uint256 param1) private pure returns (uint256) {
if((int256)(param1 - param0) >= 0x20) {//除了4个字节的函数签名,参数的编码至少是0x20字节
return sub_D8(param1, param0);
}

revert(0x0, 0x0);
}

}

如果我们追求更加细粒度的分析的话,可以查看下面逆向的伪代码。基本逻辑是一致的。

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
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;

if (var0) { revert(memory[0x00:0x00]); }

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var0 = msg.data[0x00:0x20] >> 0xe0;//右移了0xe0=14*16=224位,所以保留了了高32位,四个字节的函数签名

if (var0 == 0x2e64cec1) {
// Dispatch table entry for retrieve()
var var1 = 0x0043;
var1 = func_0075();
var temp0 = var1;
var1 = 0x0050;
var var2 = temp0;
var var3 = memory[0x40:0x60];
var1 = func_00A1(var2, var3);
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + var1 - temp1];
} else if (var0 == 0x6057361d) {
// Dispatch table entry for store(uint256)
var1 = 0x0073;
var2 = 0x006e;
var var4 = 0x04;
var3 = var4 + (msg.data.length - var4);
var2 = func_00ED(var3, var4);
func_006E(var2);
stop();
} else { revert(memory[0x00:0x00]); }
}

function func_006E(var arg0) {
storage[0x00] = arg0;
}

function func_0075() returns (var r0) { return storage[0x00]; }

function func_0088(var arg0) returns (var r0) { return arg0; }

function func_0092(var arg0, var arg1) {
var var0 = 0x009b;
var var1 = arg1;
var0 = func_0088(var1);
memory[arg0:arg0 + 0x20] = var0;
}

function func_00A1(var arg0, var arg1) returns (var r0) {
var temp0 = arg1;
var var0 = temp0 + 0x20;
var var1 = 0x00b6;
var var2 = temp0;
var var3 = arg0;
func_0092(var2, var3);
return var0;
}

function func_00C1(var arg0) {
var var0 = 0x00ca;
var var1 = arg0;
var0 = func_0088(var1);

if (arg0 == var0) { return; }
else { revert(memory[0x00:0x00]); }
}

function func_00D8(var arg0, var arg1) returns (var r0) {
var var0 = msg.data[arg1:arg1 + 0x20];
var var1 = 0x00e7;
var var2 = var0;
func_00C1(var2);
return var0;
}

function func_00ED(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;

if (arg0 - arg1 i>= 0x20) {
var var1 = 0x00;
var var2 = 0x0111;
var var3 = arg0;
var var4 = arg1 + var1;
return func_00D8(var3, var4);
} else {
var1 = 0x0102;
revert(memory[0x00:0x00]);
}
}
}

控制流

if 分支

我们观察最简单的 if 分支开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

uint256 number;

function store(uint256 num) public {
if(num <=1){
number = 17;
} else if (num < 10) {
number = 2**num;
} else {
number =num;
}

}

function retrieve() public view returns (uint256){
return number;
}
}

下面是逆向后伪代码,请跟着注释,仔细阅读。我们只关注 store 函数的内容。

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
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;

if (var0) { revert(memory[0x00:0x00]); }

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var0 = msg.data[0x00:0x20] >> 0xe0;//左移14*16=224位,剩下函数签名的32位

if (var0 == 0x2e64cec1) {
// Dispatch table entry for retrieve()
var var1 = 0x0043;
var1 = func_0075();
var temp0 = var1;
var1 = 0x0050;
var var2 = temp0;
var var3 = memory[0x40:0x60];
var1 = func_00D9(var2, var3);
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + var1 - temp1];
} else if (var0 == 0x6057361d) {//这是我们需要的函数签名
// Dispatch table entry for store(uint256)
var1 = 0x0073;
var2 = 0x006e;
var var4 = 0x04;
var3 = var4 + (msg.data.length - var4);
var2 = func_0125(var3, var4);//(msg.data的长度和函数签名的长度),取出来数据
func_006E(var2);//开始控制流
stop();
} else { revert(memory[0x00:0x00]); }
}

function func_006E(var arg0) {
if (arg0 <= 0x01) {
storage[0x00] = 0x11;
goto label_00BD;
} else if (arg0 >= 0x0a) {
storage[0x00] = arg0;
return;
} else {
var var0 = 0x00a9;
var var1 = arg0;//num
var var2 = 0x02;
var0 = func_02B4(var1, var2);//(num,2),处理指数运算还是较为复杂的
storage[0x00] = var0;

label_00BD:
return;
}
}

function func_0075() returns (var r0) { return storage[0x00]; }

function func_00C0(var arg0) returns (var r0) { return arg0; }

function func_00CA(var arg0, var arg1) {
var var0 = 0x00d3;
var var1 = arg1;
var0 = func_00C0(var1);
memory[arg0:arg0 + 0x20] = var0;
}

function func_00D9(var arg0, var arg1) returns (var r0) {
var temp0 = arg1;
var var0 = temp0 + 0x20;
var var1 = 0x00ee;
var var2 = temp0;
var var3 = arg0;
func_00CA(var2, var3);
return var0;
}

function func_00F9(var arg0) {
var var0 = 0x0102;
var var1 = arg0;
var0 = func_00C0(var1);//num

if (arg0 == var0) { return; }
else { revert(memory[0x00:0x00]); }
}

function func_0110(var arg0, var arg1) returns (var r0) {//取出数据
var var0 = msg.data[arg1:arg1 + 0x20];//取出来数据
var var1 = 0x011f;
var var2 = var0;
func_00F9(var2);//(uint256 num),也是异常处理
return var0;
}

function func_0125(var arg0, var arg1) returns (var r0) {//(msg.data.length, 0x04),input 异常处理
var var0 = 0x00;

if (arg0 - arg1 >= 0x20) {
var var1 = 0x00;
var var2 = 0x0149;
var var3 = arg0;
var var4 = arg1 + var1;
return func_0110(var3, var4);//(msg.data.length, 0x04)
} else { //传入的input异常
var1 = 0x013a;
revert(memory[0x00:0x00]);
}
}

function func_0181(var arg0) returns (var r0) { return arg0 >> 0x01; }

//(最大值、指数、底数、1)
function func_018E(var arg0, var arg1, var arg2, var arg3) returns (var r0, var arg0) {
var var0 = arg3;//1
var var1 = arg2;//底数2


//var1 是指数计算的中间结果、var2 是指数的中间结果,同时arg1会同步var2

if (arg1 <= 0x01) {//这是出口
label_01D8:
arg0 = var1;//临时结果1,指数是2的幂次
r0 = var0;//临时结果2,处理其余的指数

//两个临时结果的乘积就是最终的结果
return r0, arg0;
} else { //指数大于1
label_01A2:
var temp0 = var1;//临时计算结果
if (temp0 > arg0 / temp0) { //判断溢出,平方后超过最大值
var2 = 0x01b3;
//这是返回的错误编码,和EVM有关
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
} else if (!(arg1 & 0x01)) {//如果指数的最低位是0,说明是偶数,可以“折半”计算
var temp1 = var1;//初始值是2,这里其实是临时结果
var1 = temp1 * temp1;//平方
var var2 = 0x01d1;
var var3 = arg1;//指数num
var2 = func_0181(var3);//指数右移一位,也就是除以2

//例如2^8,指数arg1=8,然后
//第一轮计算后arg1=4,var1=2^2.
//第二轮 arg1=2,var1=2^4
//第三轮 arg1=1,var1=2^8,当arg=1时,则跳出循环

//但是可能出现指数不为1的情况,例如 2^11,
//第一轮,var1=2^2,var0=2,arg1=5
//第二轮,var1=2^4,var0=2^3,arg1=2
//第三轮,var1=2^8,var0=2^3,arg1=1

label_01D1:
arg1 = var2;//更新指数

if (arg1 <= 0x01) { goto label_01D8; }//指数小于等于1,就结束
else { goto label_01A2; }//重复计算
} else { //如果不能“折半”计算
var temp2 = var1;//临时结果
var0 = var0 * temp2;//这里采用var0,来记录
var1 = temp2 * temp2;//继续平方
var2 = 0x01d1;
var3 = arg1;
var2 = func_0181(var3);//继续更新指数,单数注意arg1没有更新
goto label_01D1;
}
}
}

function func_01E1(var arg0, var arg1, var arg2) returns (var r0) {(最大值,指数,底数)
var var0 = 0x00;//初始化返回值

if (!arg1) {//指数为0,这是比较特殊的情况,直接返回1
var0 = 0x01;
goto label_02AD;
} else if (arg2) {//指数不为0,且底数不为0
var var1 = arg2;//2

if (var1 == 0x01) {//如果底数是1,那么直接返回1
var0 = 0x01;

label_02AD:
return var0;
} else if (var1 != 0x02) {//如果底数不是2,且指数不为0,
//这里这样做的原因应该是,如果底数是2,那不就不用复杂计算,直接移位即可。

var temp0 = arg1;//num
var temp1 = arg2;//2

//这是两个临界值,如果超过了,编译器就会报错。
if (!(((temp1 < 0x0b) & (temp0 < 0x4e)) | ((temp1 < 0x0133) & (temp0 < 0x20)))) {
var1 = 0x0290;
var2 = arg0;//最大值
var var3 = arg1;//num
var var4 = arg2;//2
var var5 = 0x01;
var1, var2 = func_018E(var2, var3, var4, var5);//两个结果的乘积是最终结果
arg2 = var2;//指数是2的次幂的部分
var0 = var1;

if (var0 <= arg0 / arg2) {//这也是异常处理,结果需要小于最大值,否则异常处理
var0 = var0 * arg2;
goto label_02AD;
} else {
var1 = 0x02a6;
goto label_0152;
}
} else {
var0 = arg2 ** arg1;
//如果不满足以上的两个边界,那么直接计算,再判断是否溢出
if (var0 <= arg0) { goto label_02AD; }

var1 = 0x027d;
goto label_0152;
}
} else if (arg1 <= 0xff) {//底数等于2,但指数小于等于0xff,这也是溢出条件。
var0 = 0x02 ** arg1;

if (var0 <= arg0) { goto label_02AD; }

var var2 = 0x0247;

label_0152:
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
} else {//
var2 = 0x0230;
goto label_0152;
}
} else {//指数不为0,但是底数为0,直接返回0
var0 = 0x00;
goto label_02AD;
}
}

function func_02B4(var arg0, var arg1) returns (var r0) {//(num,2),计算2**num
var var0 = 0x00;
var var1 = 0x02bf;
var var2 = arg1;//2
var1 = func_00C0(var2);//2
arg1 = var1;//2
var1 = 0x02ca;
var2 = arg0;//num
var1 = func_00C0(var2);//num
var temp0 = var1;//num
arg0 = temp0;//num
var1 = 0x02f7;

//这是最大值,因为uint256、uint20,等等类型都有不同的最大值,也就是每一位为1。
var2 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
var var3 = arg0;//num
var var4 = arg1;//2
return func_01E1(var2, var3, var4);(最大值,指数,底数)
}
}

其中值得注意的是:

  1. if 分支在字节码中体现的逻辑很简单,但是它不一定会按照源码中 if …else if …else 匹配的顺序,而是可能调整匹配逻辑和匹配顺序。读者可以对比下面两个函数
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
//源码中
function store(uint256 num) public {
if(num <=1){
number = 17;
} else if (num < 10) {
number = 2**num;
} else {
number =num;
}

}
//字节码中
function func_006E(var arg0) {
if (arg0 <= 0x01) {
storage[0x00] = 0x11;
goto label_00BD;
} else if (arg0 >= 0x0a) {
storage[0x00] = arg0;
return;
} else {
var var0 = 0x00a9;
var var1 = arg0;
var var2 = 0x02;
var0 = func_02B4(var1, var2);//2**num,处理指数运算还是较为复杂的
storage[0x00] = var0;

label_00BD:
return;
}
}
  1. 指数的赋值操作较为复杂,它的基本逻辑如下:
  • 如果指数是 0,那么直接返回 1.
  • 如果指数和底数都不是 0。
    • 如果底数是 1,那么结果一定是 1。
    • 如果底数不是 2,判断边界条件,如果符合就采用“折半“计算,如果不符合先计算,在判断是否溢出。
    • 如果底数是 2,那么直接计算,实际上只要移位即可。
  • 如果指数不是 0,但是底数是 0,那么直接返回 0.

折半计算的规则:

  • 折半计算。如果指数是 2 的倍数,那么就折半,然后迭代计算。
  • 如果指数不是 2 的倍数,那么就会引入额外的变量,用于存储指数不是 2 的幂次的部分。
  • 两部分的乘积就是最终的结果。
  1. 字节码通过跳转实现一定层次的模块化,我们仔细观察字节码可以发现,这样的模块化,其实有利于实现完备性。例如在计算指数之后,进一步判断是否溢出,添加了如下的逻辑,arg0 是该类型支持的最大的数。var0 * var2 是最终的结果。
1
2
3
4
5
6
7
if (var0 <= arg0 / arg2) {//这也是异常处理,结果需要小于最大值,否则异常处理
var0 = var0 * arg2;
goto label_02AD;
} else {
var1 = 0x02a6;
goto label_0152;
}

如果我们计算指数时,添加 unchecked{},估计就会省略这一部分的代码。当然也有其他部分的改动。

for 循环 和 unchecked

有群友发送过优化 gas 的表情包,我们将从字节码的层次分别探讨。图片来自此处

FTY_lIoWQAU4O5i

图中提到了优化 gas 种方法,我们其中对 for 循环的修改和 unchecked

  1. for(uint i = 0; i<length; i++)
  2. for(uint i = 0; i<length; ++i)
  3. for(uint i = 0; i<length; ){[some logics];unchecked{++i}}

第一种情况的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

uint256[3] number;

function store(uint256 num) public {
for(uint i= 0; i < number.length; i++){
number[i] = num +i;
}
}

function retrieve() public view returns ( uint256[3] memory){
return number;
}
}

逆向后代码如下,关注 store 函数,赋值逻辑是函数 func_006E

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
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;

if (var0) { revert(memory[0x00:0x00]); }

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var0 = msg.data[0x00:0x20] >> 0xe0;

if (var0 == 0x2e64cec1) {
// Dispatch table entry for retrieve()
var var1 = 0x0043;
var1 = func_0075();
var temp0 = var1;
var1 = 0x0050;
var var3 = memory[0x40:0x60];
var var2 = temp0;
var1 = func_01DF(var2, var3);
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + var1 - temp1];
} else if (var0 == 0x6057361d) {
// Dispatch table entry for store(uint256)
var1 = 0x0073;
var2 = 0x006e;
var var4 = 0x04;
var3 = var4 + (msg.data.length - var4);
//取参数并完成一些校验
var2 = func_022B(var3, var4);//msg.length,0x04
func_006E(var2);
stop();
} else { revert(memory[0x00:0x00]); }
}

function func_006E(var arg0) {
var var0 = 0x00;

if (var0 >= 0x03) {
label_0104:
return;
} else {
label_00CD:
var var1 = 0x00d8;
var var2 = var0;//索引 i
var var3 = arg0;//传入的num
var1 = func_0287(var2, var3);//含溢出检查的加法
var2 = 0x00;
var3 = var0;

//检查是否越界
if (var3 < 0x03) {
storage[var3 + var2] = var1;
var1 = var0;
var3 = var1;
var2 = 0x00fc;
var2 = func_030C(var3);//含溢出检查的+1,这是索引 i 的处理
var0 = var2;

if (var0 >= 0x03) { goto label_0104; }
else { goto label_00CD; }
} else {
var var4 = 0x00eb;
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x32;
revert(memory[0x00:0x24]);
}
}
}

function func_0075() returns (var r0) {
var var0 = 0x007d;
var0 = func_0108();
var temp0 = memory[0x40:0x60];
memory[0x40:0x60] = temp0 + 0x20 * 0x03;
var var1 = temp0;
var var2 = 0x00;
var var4 = var1;
var var3 = 0x03;
var var5 = var2;
var var6 = 0x03;

if (!var6) {
label_00B6:
return var1;
} else {
var temp1 = var4;
var temp2 = temp1 + var6 * 0x20;
var4 = temp2;
var temp3 = var5;
memory[temp1:temp1 + 0x20] = storage[temp3];
var5 = temp3 + 0x01;
var6 = temp1 + 0x20;

if (var4 <= var6) { goto label_00B6; }

label_00A2:
var temp4 = var5;
var temp5 = var6;
memory[temp5:temp5 + 0x20] = storage[temp4];
var6 = temp5 + 0x20;
var5 = temp4 + 0x01;

if (var4 > var6) { goto label_00A2; }
else { goto label_00B6; }
}
}

function func_0108() returns (var r0) {
var temp0 = memory[0x40:0x60];
memory[0x40:0x60] = temp0 + 0x60;
memory[temp0:temp0 + 0x03 * 0x20] = msg.data[msg.data.length:msg.data.length + 0x03 * 0x20];
return temp0;
}

function func_012A(var arg0) returns (var r0) { return 0x03; }

function func_0135(var arg0, var arg1) returns (var r0) { return arg1; }

function func_0140(var arg0) returns (var r0) { return arg0; }

function func_014A(var arg0) returns (var r0) { return arg0; }

function func_0154(var arg0, var arg1) {
var var0 = 0x015d;
var var1 = arg1;
var0 = func_014A(var1);
memory[arg0:arg0 + 0x20] = var0;
}

function func_0163(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;
var var1 = 0x016f;
var var2 = arg0;
var var3 = arg1;
func_0154(var2, var3);
return arg0 + 0x20;
}

function func_017B(var arg0) returns (var r0) { return arg0 + 0x20; }

function func_0188(var arg0, var arg1) {
var var0 = 0x0191;
var var1 = arg1;
var0 = func_012A(var1);
var1 = 0x019b;
var var2 = var0;
var var3 = arg0;
var1 = func_0135(var2, var3);
arg0 = var1;
var1 = 0x01a6;
var2 = arg1;
var1 = func_0140(var2);
var2 = var1;
var3 = 0x00;

if (var3 >= var0) {
label_01D7:
return;
} else {
label_01B3:
var var4 = memory[var2:var2 + 0x20];
var var5 = 0x01be;
var var6 = arg0;
var var7 = var4;
var5 = func_0163(var6, var7);
arg0 = var5;
var5 = 0x01c9;
var6 = var2;
var5 = func_017B(var6);
var2 = var5;
var3 = var3 + 0x01;

if (var3 >= var0) { goto label_01D7; }
else { goto label_01B3; }
}
}

function func_01DF(var arg0, var arg1) returns (var r0) {
var temp0 = arg1;
var var0 = temp0 + 0x60;
var var1 = 0x01f4;
var var2 = temp0;
var var3 = arg0;
func_0188(var2, var3);
return var0;
}

function func_01FF(var arg0) {
var var0 = 0x0208;
var var1 = arg0;
var0 = func_014A(var1);

if (arg0 == var0) { return; }
else { revert(memory[0x00:0x00]); }
}

function func_0216(var arg0, var arg1) returns (var r0) {
var var0 = msg.data[arg1:arg1 + 0x20];//参数num
var var1 = 0x0225;
var var2 = var0;
func_01FF(var2);
return var0;
}

function func_022B(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;

if (arg0 - arg1 i>= 0x20) {
var var1 = 0x00;
var var2 = 0x024f;
var var3 = arg0;
var var4 = arg1 + var1;
return func_0216(var3, var4);//msg.length, 0x04
} else {
var1 = 0x0240;
revert(memory[0x00:0x00]);
}
}

//加法溢出检查,处理逻辑
function func_0287(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;
var var1 = 0x0292;
var var2 = arg1;//num
var1 = func_014A(var2);//num
arg1 = var1;
var1 = 0x029d;
var2 = arg0;//00
var1 = func_014A(var2);//00
arg0 = var1;//num

if (arg1 <= 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - arg0) { return arg1 + arg0; }

var1 = 0x02d1;
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
}

function func_030C(var arg0) returns (var r0) {
var var0 = 0x00;
var var1 = 0x0317;
var var2 = arg0;
var1 = func_014A(var2);
arg0 = var1;

if (arg0 != 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) { return arg0 + 0x01; }

var1 = 0x0349;
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
}
}

读者可能跟着有点困难,这里详细说明。for 循环的处理,每一步的加法都是会检查溢出的,它的字节码逻辑也很浅显,是非常常规的循环。

第二种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

uint256[3] number;

function store(uint256 num) public {
uint length = number.length;
for(uint i= 0; i < length; ++i){
number[i] = num +i;
}
}

function retrieve() public view returns ( uint256[3] memory){
return number;
}
}

绝大部分代码和第一种情况类似,因此只给出关键部分的反编译代码。下面是 ++i 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function func_0310(var arg0) returns (var r0) {
var var0 = 0x00;
var var1 = 0x031b;
var var2 = arg0;
var1 = func_014E(var2);
arg0 = var1;

if (arg0 != 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) { return arg0 + 0x01; }

var1 = 0x034d;
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
}

i++ 代码对比,可以发现在处理加法的过程没有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function func_030C(var arg0) returns (var r0) {
var var0 = 0x00;
var var1 = 0x0317;
var var2 = arg0;
var1 = func_014A(var2);
arg0 = var1;

if (arg0 != 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) { return arg0 + 0x01; }

var1 = 0x0349;
memory[0x00:0x20] = 0x4e487b7100000000000000000000000000000000000000000000000000000000;
memory[0x04:0x24] = 0x11;
revert(memory[0x00:0x24]);
}

其他部分的反汇编结果在逻辑上是完全一致的。那我们从最简单的情况分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Test {

uint256 number;

function increment1() public {
number++;
}

function increment2() public {
++number;
}
}

发现函数 increment1() 消耗 49963 gas,而 increment2() 消耗 49982 gas,比第一个函数高。而在其他的合约里,这个结果可能截然相反。这只能解释为:为了保持完备性,i++++i 在堆栈的处理顺序不一样,方便与其他部分兼容。在执行的逻辑上是完全一致的。

第三种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

uint256[3] number;

function store(uint256 num) public {
uint length = number.length;
for(uint i= 0; i < length;){
number[i] = num +i;
unchecked{ i++;}
}
}

function retrieve() public view returns ( uint256[3] memory){
return number;
}
}

我们可以大致猜测到,需要检查溢出的部分逻辑,将会省略,而执行的逻辑的变化几乎可以忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
label_00D2:
var var2 = 0x00dd;
var var3 = var1;
var var4 = arg0;
var2 = func_0287(var3, var4);
var3 = 0x00;
var4 = var1;

if (var4 < 0x03) {
storage[var4 + var3] = var2;
var1 = var1 + 0x01;

if (var1 >= var0) { goto label_0103; }
else { goto label_00D2; }
}

核心处理部分和我们的猜想完全一致。采用 var1 = var1 + 0x01 代替了含有溢出检查的加法。

delegatecall

读者应该熟悉 delegatecall 的功能,存储布局不冲突的情况下(存储布局一致或者一方存储空间未修改),上下文是调用发起者的上下文。笔者猜测,ABI 编码会作为新的 msg.data,然后上下文切换会通过 DELEGATECALL 操作码完成。

功能简介

先简单介绍操作码的含义,详情见 https://www.evm.codes/#f4 。读者需要理解下面的这句话。

Creates a new sub context as if calling itself, but with the code of the given account. In particular the storage, the current sender and the current value remain the same. Note that an account with no code will return success as true.

下面是对 gas 的限制。

From the Tangerine Whistle fork, gas is capped at all but one 64th (remaining_gas / 64) of the remaining gas of the current context. If a call tries to send more, the gas is changed to match the maximum allowed.

输入条件如下,指定了 gas、合约地址、调用的参数的偏移量和大小,返回值的偏移量和大小。可以看出,内存是媒介。

  1. gas: amount of gas to send to the sub context to execute. The gas that is not used by the sub context is returned to this one.
  2. address: the account which code to execute.
  3. argsOffset: byte offset in the memory in bytes, the calldata of the sub context.
  4. argsSize: byte size to copy (size of the calldata).
  5. retOffset: byte offset in the memory in bytes, where to store the return data of the sub context.
  6. retSize: byte size to copy (size of the return data).

参数编码

首先,bytes 默认是 ascii 编码,所以如果直接输入 bytes 类型,应该使用十六进制的字面常量,如 hex"d09de08a".

所以看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Test {

uint256 public number;

function increment() public {
number++;
}
}

contract delegate {

uint256 public number;
function call(address addr) public {
addr.delegatecall(bytes(hex"d09de08a"));
}
}

下面是反编译代码:

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
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;

if (var0) { revert(memory[0x00:0x00]); }

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var0 = msg.data[0x00:0x20] >> 0xe0;

if (var0 == 0x8381f58a) {
// Dispatch table entry for number()
var var1 = 0x0043;
var var2 = func_0075();
var temp0 = var2;
var2 = 0x0050;
var var3 = temp0;
var var4 = memory[0x40:0x60];
var2 = func_0134(var3, var4);
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + var2 - temp1];
} else if (var0 == 0xf55332ab) {
// Dispatch table entry for call(address)
var1 = 0x0073;
var2 = 0x006e;
var4 = 0x04;
var3 = var4 + (msg.data.length - var4);
var2 = func_01B2(var3, var4);//取参数 address
func_006E(var2);
stop();
} else { revert(memory[0x00:0x00]); }
}

function func_006E(var arg0) {
var var0 = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
var temp0 = memory[0x40:0x60];//memorysize
memory[0x40:0x60] = temp0 + 0x40;//memorysize += 0x40
memory[temp0:temp0 + 0x20] = 0x04;//argsize
memory[temp0 + 0x20:temp0 + 0x20 + 0x20] = 0xd09de08a00000000000000000000000000000000000000000000000000000000;//arg
var var1 = 0x00d5;
var var2 = temp0;//数去起始位置
var var3 = memory[0x40:0x60];
var1 = func_0259(var2, var3);
var temp1 = memory[0x40:0x60];
var temp2;
temp2, memory[temp1:temp1 + 0x00] = address(var0).delegatecall.gas(msg.gas)(memory[temp1:temp1 + var1 - temp1]);
var1 = returndata.length;
var2 = var1;

if (var2 == 0x00) { return; }

var temp3 = memory[0x40:0x60];
var1 = temp3;
memory[0x40:0x60] = var1 + (returndata.length + 0x3f & ~0x1f);
memory[var1:var1 + 0x20] = returndata.length;
var temp4 = returndata.length;
memory[var1 + 0x20:var1 + 0x20 + temp4] = returndata[0x00:0x00 + temp4];
}

function func_0075() returns (var r0) { return storage[0x00]; }

function func_011B(var arg0) returns (var r0) { return arg0; }

function func_0125(var arg0, var arg1) {
var var0 = 0x012e;
var var1 = arg1;
var0 = func_011B(var1);
memory[arg0:arg0 + 0x20] = var0;
}

function func_0134(var arg0, var arg1) returns (var r0) {
var temp0 = arg1;
var var0 = temp0 + 0x20;
var var1 = 0x0149;
var var2 = temp0;
var var3 = arg0;
func_0125(var2, var3);
return var0;
}

function func_0154(var arg0) returns (var r0) { return arg0 & 0xffffffffffffffffffffffffffffffffffffffff; }

function func_0174(var arg0) returns (var r0) {
var var0 = 0x00;
var var1 = 0x017f;
var var2 = arg0;
return func_0154(var2);
}

function func_0186(var arg0) {
var var0 = 0x018f;
var var1 = arg0;
var0 = func_0174(var1);

if (arg0 == var0) { return; }
else { revert(memory[0x00:0x00]); }
}

function func_019D(var arg0, var arg1) returns (var r0) {
var var0 = msg.data[arg1:arg1 + 0x20];
var var1 = 0x01ac;
var var2 = var0;
func_0186(var2);
return var0;
}

function func_01B2(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;

if (arg0 - arg1 i>= 0x20) {
var var1 = 0x00;
var var2 = 0x01d6;
var var3 = arg0;
var var4 = arg1 + var1;
return func_019D(var3, var4);
} else {
var1 = 0x01c7;
revert(memory[0x00:0x00]);
}
}

function func_01DF(var arg0) returns (var r0) { return memory[arg0:arg0 + 0x20]; }

function func_01EA(var arg0, var arg1) returns (var r0) { return arg1; }

function func_01F5(var arg0, var arg1, var arg2) {
var var0 = 0x00;

if (var0 >= arg0) {
label_0213:

if (var0 <= arg0) { return; }

memory[arg1 + arg0:arg1 + arg0 + 0x20] = 0x00;
return;
} else {
label_0201:
var temp0 = var0;
memory[arg1 + temp0:arg1 + temp0 + 0x20] = memory[arg2 + temp0:arg2 + temp0 + 0x20];
var0 = temp0 + 0x20;

if (var0 >= arg0) { goto label_0213; }
else { goto label_0201; }
}
}

function func_0228(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;
var var1 = 0x0233;
var var2 = arg1;
var1 = func_01DF(var2);
var2 = 0x023d;
var var3 = var1;
var var4 = arg0;
var2 = func_01EA(var3, var4);
var temp0 = var2;
arg0 = temp0;
var2 = 0x024d;
var3 = var1;
var4 = arg0;
var var5 = arg1 + 0x20;
func_01F5(var3, var4, var5);
return arg0 + var1;
}

function func_0259(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;
var var1 = 0x0265;
var var2 = arg1;
var var3 = arg0;
return func_0228(var2, var3);
}
}

跨合约调用存在内存的传递,很复杂。

可见性

internal

internal 函数和 C 语言的 inline 相似,都会在函数调用出直接展开,没有函数签名。我分别分析了下面三种情况:

直接写在函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;


contract TestMath{
function _increace(uint y) internal pure returns (uint z) {
z = y+1;
}
function increace(uint x) public pure returns (uint) {
return _increace(x);
}

}

库函数:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

library Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// else z = 0 (default value)
}

function increace(uint y) internal pure returns (uint z) {
z = y+1;
}
}

contract TestMath {
function increace(uint x) public pure returns (uint) {
return Math.increace(x);
}

}

继承:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// else z = 0 (default value)
}

function _increace(uint y) internal pure returns (uint z) {
z = y+1;
}
}

contract TestMath is Math {
function increace(uint x) public pure returns (uint) {
return Math._increace(x);
}

}

逻辑都是完全一样的。

值得注意的是,继承或者库函数调用的过程中,dead code 会自动被筛选,只出现使用了的代码

特殊函数

Getter

receive

fallback