第5节:function执行流程(if else function)
Reversing and debugging EVM Smart contracts: The Execution flow if/else/for/functions(part5)
版本
- Version:0.8.7
- Optimizer:true,Runs:1
1. IF/ELSE
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow(bool x) external {
if (x) {
value = 4;
} else {
value = 9;
}
}
}
汇编:(medium原文描述应关闭优化,与汇编数据不符,实际上应该选择 runs:1)
062 JUMPDEST |0x01|stack after arguments discarded|
063 DUP1 |0x01|0x01|
064 ISZERO |0x00|0x01|
065 PUSH1 4b |0x4b|0x00|0x01|
067 JUMPI |0x01|
068 PUSH1 04 |0x04|0x01|
070 PUSH1 00 |0x00|0x04|0x01|
072 SSTORE |0x01|
073 POP
074 JUMP
075 JUMPDEST
076 PUSH1 09
078 PUSH1 00
080 SSTORE
081 POP
082 JUMP
- 只要有JUMPI,那么就一定有if或for。
2. ELSE IF
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow(uint x) external {
if (x == 1) {
value = 4;
} else if (x == 2) {
value = 9;
} else if (x == 3) {
value = 14;
} else if (x == 4) {
value = 19;
} else {
value = 24;
}
}
}
汇编:
062 JUMPDEST
063 DUP1
064 PUSH1 01
066 EQ
067 ISZERO
068 PUSH1 4e
070 JUMPI
071 PUSH1 04
073 PUSH1 00
075 SSTORE
076 POP
077 JUMP
078 JUMPDEST
079 DUP1
080 PUSH1 02
082 EQ
083 ISZERO
084 PUSH1 5e
086 JUMPI
087 PUSH1 09
089 PUSH1 00
091 SSTORE
092 POP
093 JUMP
094 JUMPDEST
095 DUP1
096 PUSH1 03
098 EQ
099 ISZERO
100 PUSH1 6e
102 JUMPI
103 PUSH1 0e
105 PUSH1 00
107 SSTORE
108 POP
109 JUMP
110 JUMPDEST
111 DUP1
112 PUSH1 04
114 EQ
115 ISZERO
116 PUSH1 7e
118 JUMPI
119 PUSH1 13
121 PUSH1 00
123 SSTORE
124 POP
125 JUMP
126 JUMPDEST
127 PUSH1 18
129 PUSH1 00
131 SSTORE
132 POP
133 JUMP
汇编很长,但是逻辑并不复杂,只是在不断的判断和跳转,有点像我们之前需要函数的selector,按照汇编的逻辑反推,我们可以得到下面的逻辑代码,不断的进行嵌套、判断:
if (x == 1) {
// do something
} else {
if (x == 2) {
// do something
} else {
if (x == 3) {
// do something
} else {
if (x == 4) {
// do something
} else {
// do something
}
}
}
}
3. For Loops
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow(uint x) external {
for (uint i = 0; i < x; i++) {
value += i;
}
}
}
我们执行完一个函数,然后点击debug,此时一般都会停在一个JUMPDEST的OPCODE上,因为里就是函数执行的入口,此时stack V中已经压入了函数参数,例如这里:
# 汇编如下:
062 JUMPDEST
063 PUSH1 00 # for里面的变量,i = 0
065 JUMPDEST
066 DUP2
067 DUP2
068 LT # for里面的小于号, <
069 ISZERO
070 PUSH1 6c
072 JUMPI # 当条件不满足时,跳出循环♻️
# 当前stack数据:
[
"0x000000000000000000000000000000000000000000000000000000000000000a", <=== 这里就是参数x=10(0a是十六进制)
"0x000000000000000000000000000000000000000000000000000000000000003c",
"0x0000000000000000000000000000000000000000000000000000000047f4d98d"
]
- 函数参数都是存储在stack中的
073 DUP1
074 PUSH1 00
076 DUP1
077 DUP3
078 DUP3
079 SLOAD # 将0存储到状态变量中
080 PUSH1 57
082 SWAP2
083 SWAP1
084 PUSH1 88
086 JUMP
- 接下来会校验加法是否溢出,在solidity 0.8之后,会自动校验,生成相应的汇编代码
136 JUMPDEST
137 PUSH1 00
139 DUP3
140 NOT
141 DUP3
142 GT
143 ISZERO
144 PUSH1 98
146 JUMPI
147 PUSH1 98
149 PUSH1 b5
151 JUMP
- - if yes, then the code JUMP to B5 and reverts. (You can check disassembly at 181.) - if not, the execution continues at 98 (152 in dec)
152 JUMPDEST
153 POP
154 ADD
155 SWAP1
156 JUMP
- 经过溢出校验之后,上面的代码会将slot0的数据+i。
087 JUMPDEST
088 SWAP1
089 SWAP2
090 SSTORE
091 POP
092 DUP2
093 SWAP1
094 POP
095 PUSH1 65
097 DUP2
098 PUSH1 9d
100 JUMP
- 这段代码会将slot0存储起来(SSTORE),然后跳转到9d(byte 157),然后重新校验是否溢出。
157 JUMPDEST
158 PUSH1 00
160 PUSH1 00
162 NOT
163 DUP3
164 EQ
165 ISZERO
166 PUSH1 ae
168 JUMPI
169 PUSH1 ae
171 PUSH1 b5
173 JUMP
- 如果一切正常,继续跳转到ae(174)
174 JUMPDEST
175 POP
176 PUSH1 01
178 ADD
179 SWAP1
180 JUMP
- 继续加1,然后跳转到65(101)
101 JUMPDEST
102 SWAP2
103 POP
104 POP
105 PUSH1 41
107 JUMP
这里的目的是clean整个Stack上的数据,然后跳转到41(65),跳转到00
整体反推后,逻辑为:
I declare
i = 0
.- I test if
i < x
if yes jump directly to the end (8). - Load the Slot 0 (
value
variable). - verify that when, i’ll add i to Slot to
value
, there won’t be an overflow. If the test fails go to 181 when the function reverts. - add i to
value
and SSTORE to slot 0. - verify that when the EVM will add 1 to i (for incrementation) there won’t be an overflow if test fails go to 181 when the function reverts.
- add 1 to i and return to 2.
- end the execution.
The loop lies between 2 and 8 while i < x (in this example we called flow() with x = 10)
4. function无参数(重要!!)
Compile it WITHOUT the optimizer (but still with solidity version 0.8.7)
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow() external {
flow2();
}
function flow2() public {
value = 5;
}
}
flow内调用flow2的汇编如下:
071 JUMPDEST
072 PUSH1 4d // byte 77
074 PUSH1 4f // byte 79
076 JUMP
077 JUMPDEST
078 JUMP
079 JUMPDEST
080 PUSH1 05
082 PUSH1 00
084 DUP2
085 SWAP1
086 SSTORE
087 POP
088 JUMP
分析上述代码,得出结论:函数执行前后,stack中的数据一致。
- 执行flow2之前,stack(0)= 4d(byte 77)
- 执行flow2之后,stack(0)= 4d (byte 77)
THIS IS THE CASE FOR ALL FUNCTIONS !!! ALL FUNCTIONS IN SOLIDITY ONCE EXECUTED WILL USE THE STACK AND CLEAN IT AFTER EXECUTION. AS A RESULT THE STACK WILL EXACTLY THE SAME BEFORE AND AFTER EXECUTION !
所以当flow2执行后,在byte 88的jump语句,会跳转到4d(byte 77)位置。为什么会跳转到这里呢?因为flow2是嵌套在flow中的,当flow2执行完毕后,函数需要回到flow中,因此才会有语句PUSH1 4d,这是为了存储flow的状态。
这也侧面印证了函数的调用特点,执行完字函数(此处是flow2)之后,所有字函数内的变量都会清除掉。
5. Function有参数
Compile it WITHOUT the optimizer (but still with solidity version 0.8.7)
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow() external {
flow2(5);
}
function flow2(uint y) public {
value = y;
}
}
byte 97是flow入口(居然有特殊颜色提示)
94和95的两个pop,会将stack上的数据清除掉,恢复到执行byte 97 (flow)之前的stack状态!
6. Function有返回值
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow() external {
uint n = flow2();
value = n;
}
function flow2() public returns(uint) {
return 5;
}
}
7. Let’s bring it together
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
uint value = 0;
function flow() external {
uint n = flow2(5,7);
value = n;
}
function flow2(uint x,uint y) public returns(uint) {
return x;
}
}
汇编:
117 JUMPDEST
118 PUSH1 00
120 PUSH2 0083
123 PUSH1 05
125 PUSH1 07
127 PUSH2 008f
130 JUMP
131 JUMPDEST
132 SWAP1
133 POP
134 DUP1
135 PUSH1 00
137 DUP2
138 SWAP1
139 SSTORE
140 POP
141 POP
142 JUMP
143 JUMPDEST
144 PUSH1 00
146 DUP3
147 SWAP1
148 POP
149 SWAP3
150 SWAP2
151 POP
152 POP
153 JUMP
8. Conclusion
When you call a function in solidity (in assembly).
- The EVM PUSH all arguments to the stack before the call
- The function is executed
- ALL return values are PUSHED in the stack
本文有助于理解函数调用过程中的stack处理规则,了解即可。