编程语言虚拟机一般有两种类型,基于栈,或者基于寄存器。和JVM一样,EVM也是基于栈的虚拟机。

既然是支持栈的虚拟机,那么EVM肯定首先得有个栈。为了方便进行密码学计算,EVM采用了32字节(256比特)的字长。EVM栈以字(Word)为单位进行操作,最多可以容纳1024个字。

和JVM一样,EVM执行的也是字节码。由于操作码被限制在一个字节以内,所以EVM指令集最多只能容纳256条指令。目前EVM已经定义了约142条指令,还有100多条指令可供以后扩展。这142条指令包括算术运算指令,比较操作指令,按位运算指令,密码学计算指令,栈、memory、storage操作指令,跳转指令,区块、智能合约相关指令等。

一、开始

对于智能合约的反编译,之前一直习惯使用在线的工具:Online Solidity Decompiler
反编译实例:

  • 源码

    1
    2
    3
    4
    5
    6
    7
    contract test {
    uint b=1;
    function a(uint _a) public returns (uint c){
    c = _a+b;
    return c;
    }
    }
  • 反编译结果

    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
    contract Contract {
    function main() {
    memory[0x40:0x60] = 0x80;//分配内存256字节空间

    if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } //判断用户调用改合约输入的data长度是否小于4,如果小于4就revert

    var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; //获取data的低4位,赋值给var0

    if (var0 != 0xf0fdf834) { revert(memory[0x00:0x00]); } // `0xf0fdf834`是“test”的sha3前4位,这里如果相等,就表示用户调用的是test函数,将执行该函数

    var var1 = msg.value;

    if (var1) { revert(memory[0x00:0x00]); } //msg.value如果不等于0,就退出执行,因为test函数没有payable,不需要支付费用

    var1 = 0x6c;
    var var2 = msg.data[0x04:0x24]; //传入的参数
    var1 = func_0082(var2); //调用test(var2)
    var temp0 = memory[0x40:0x60];
    memory[temp0:temp0 + 0x20] = var1;
    var temp1 = memory[0x40:0x60];
    return memory[temp1:temp1 + (temp0 + 0x20) - temp1]; //返回结果var1
    }

    function func_0082(var arg0) returns (var r0) { return arg0 + storage[0x00]; }
    }

    该反编译结果实际上就是对执行逻辑的一个翻译,注释部分对代码进行了简单的解释
    ps:使用web3.sha3("a(uint256)");可以计算出a(uint256)的签名,计算结果为
    “0xf0fdf83467af68171df09204c0b00056c1e4c80e368b3fff732778b858f7966d”,之后取前四字节”f0fdf834”作为a(uint256)函数的签名。

但是并不是每一次使用该在线反编译工具都是这个顺利的,某一次这个网站502了,突然意识到本地的逆向工具很重要,而且有时候会遇到在线反编译报错的情况,所以用ida安装了合约逆向的插件:GitHub - crytic/ida-evm: IDA Processor Module for the Ethereum Virtual Machine (EVM)
但是ida逆向的结果和在线的Decompiler还是有差别的,在线的Decompiler反编译出来会有伪代码,比较容易理解,ida反编译的是opcode,无奈,只好去学习一下opcode相关内容。

二、简单了解opcode

PUSH1 0x60 PUSH1 0x40 MSTORE

  • PUSH1 (0x60): put 0x60 in the stack.
  • PUSH1 (0x40): put 0x40 in the stack.
  • MSTORE (0x52): allocate 0x60 of memory space and move to the 0x40 position.

实际上“6060604052”通常是在合约的开头,简而言之,“PUSH1 0x60 PUSH1 0x40 MSTORE”正在做的是分配96个字节的存储器并将指针移动到第64个字节的开头。 我们现在有64个字节用于临时空间,32个字节用于临时存储器存储。

EVM里有三种存储方式:

  • stack “PUSH”命令
  • memory 用“MSTORE”指令
  • stroage 用“SSTORE”指令存储数据,类似于存储在硬盘上,最耗费gas的存储
    针对这三种存储方式,展开又有一些内容了,

三、从solc编译过程来理解solidity合约结构 - 安全客,安全资讯平台

实例合约

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.24;

contract test {

function a() {
uint a = 123;
}

}

通过remix上编译返回的Runtime 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
000 PUSH1 80
002 PUSH1 40
004 MSTORE ; 把0x80 写到[0x40 ,0x40 + 0x20] 这块内存里面,因为内存是空的,这会创建新的内存
005 PUSH1 04
007 CALLDATASIZE ; 获取CALLDATA 的长度
008 LT ; LT 和PUSH1 0x4 对应,判断CALLDATA 的长度是否小于0x4
009 PUSH1 3f
011 JUMPI ; 如果小于0x4 就往下跳转到0x3F
012 PUSH1 00
014 CALLDATALOAD ; CALLDATALOAD 0x0 ,PUSH1 0x0 是给CALLDATALOAD 的参数,意思要获取CALLDATA 数据的偏移位置
015 PUSH29 0100000000000000000000000000000000000000000000000000000000
045 SWAP1
046 DIV ; DIV 和PUSH29 对应,意思是把上面的数据向左移28 字节,剩下4 字节是调用合约函数名的哈希
047 PUSH4 ffffffff
052 AND ; AND 和PUSH4 0xFFFFFFFF 对应,保留低位4 字节数据,高位去处
053 DUP1
054 PUSH4 0dbe671f ; 这个是合约有的函数名,经过sha3() 精简过的
059 EQ ; 判断传递过来的函数调用名是否相等
060 PUSH1 44
062 JUMPI ; 如果两值相等就往下跳转到0x44
063 JUMPDEST ; 空指令
064 PUSH1 00
066 DUP1
067 REVERT ; 没有匹配到相应的函数就撤销所有操作,Revert(0,0)
068 JUMPDEST
069 CALLVALUE ; 获取用户转帐数额
070 DUP1
071 ISZERO ; 如果用户转帐数额为0
072 PUSH1 4f
074 JUMPI ; 转帐数额不为0 则跳到0x4F,否则就退出
075 PUSH1 00
077 DUP1
078 REVERT ; 因为调用函数a() 是不需要附加转帐金额的,所以检测到带有附加金额的函数调用就退出,参考payable 关键字
079 JUMPDEST
080 POP
081 PUSH1 56
083 PUSH1 58
085 JUMP ; 跳转到地址88
086 JUMPDEST
087 STOP ; 停止执行
088 JUMPDEST
089 PUSH1 00
091 PUSH1 7b
093 SWAP1
094 POP
095 POP
096 JUMP ; 跳转到地址86
097 STOP
---- 合约代码结束分界线 ----
098 LOG1
099 PUSH6 627a7a723058
106 SHA3
107 MUL
108 PUSH15 5fd8c2f6fe4103dba9baf9c48c052e
124 CALLDATALOAD
125 INVALID
126 PUSH1 d9
128 INVALID
129 INVALID
130 TIMESTAMP
131 STATICCALL
132 INVALID
133 INVALID
134 DUP13
135 INVALID
136 TIMESTAMP
137 INVALID
138 NUMBER
139 STOP
140 INVALID
  • 函数运算过程

01221030_Z6Do的副本.png

四、remix动态调试

remix(Remix - Ethereum IDE)提供了动态调试的功能

点击左侧的debug图标,输入合约的交易hash,点击start debugging

d00ba0e8.png

同态调试时会展示opcode,stack、storage以及memory的值。

e30468bd.png

五、小结

因为这个合约本身比较简单,所以在调试过程中会感觉比较容易。分析opcode,会发现有很多看似多余的代码,不理解其意思,后续还需要多练习,以及阅读solidity的编译器源码(GitHub - ethereum/solidity: Solidity, the Contract-Oriented Programming Language)以及以太坊中evm的实现(go-ethereum/core/vm at master · ethereum/go-ethereum · GitHub)。