(三)函数调用 | Word Count: 6.2k | Reading Time: 31mins | Post Views:
(一)初步认识EVM字节码 (二)状态变量的赋值 (三)函数调用
普通函数调用
简单赋值
注意:需要区分部署时和运行时字节码 ,参考下面这张图。虽然现在的字节码有小改动,但是仍然有参考意义。
所以在编译成字节码是应该使用 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);(最大值,指数,底数) } }
其中值得注意的是:
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; } }
指数的赋值操作较为复杂,它的基本逻辑如下:
如果指数是 0,那么直接返回 1.
如果指数和底数都不是 0。
如果底数是 1,那么结果一定是 1。
如果底数不是 2,判断边界条件,如果符合就采用“折半“计算,如果不符合先计算,在判断是否溢出。
如果底数是 2,那么直接计算,实际上只要移位即可。
如果指数不是 0,但是底数是 0,那么直接返回 0.
折半计算的规则:
折半计算。如果指数是 2 的倍数,那么就折半,然后迭代计算。
如果指数不是 2 的倍数,那么就会引入额外的变量,用于存储指数不是 2 的幂次的部分。
两部分的乘积就是最终的结果。
字节码通过跳转实现一定层次的模块化,我们仔细观察字节码可以发现,这样的模块化,其实有利于实现完备性。例如在计算指数之后,进一步判断是否溢出,添加了如下的逻辑,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 的表情包,我们将从字节码的层次分别探讨。图片来自此处 。
图中提到了优化 gas 种方法,我们其中对 for 循环的修改和 unchecked
for(uint i = 0; i<length; i++)
for(uint i = 0; i<length; ++i)
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、合约地址、调用的参数的偏移量和大小,返回值的偏移量和大小。可以看出,内存是媒介。
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.
address
: the account which code to execute.
argsOffset
: byte offset in the memory in bytes, the calldata of the sub context .
argsSize
: byte size to copy (size of the calldata ).
retOffset
: byte offset in the memory in bytes, where to store the return data of the sub context .
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