编程语言虚拟机一般有两种类型,基于栈,或者基于寄存器。和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
7contract 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
26contract 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 | pragma solidity ^0.4.24; |
通过remix上编译返回的Runtime Bytecode字节码
1 | 000 PUSH1 80 |
- 函数运算过程
四、remix动态调试
remix(Remix - Ethereum IDE)提供了动态调试的功能
点击左侧的debug图标,输入合约的交易hash,点击start debugging
同态调试时会展示opcode,stack、storage以及memory的值。
五、小结
因为这个合约本身比较简单,所以在调试过程中会感觉比较容易。分析opcode,会发现有很多看似多余的代码,不理解其意思,后续还需要多练习,以及阅读solidity的编译器源码(GitHub - ethereum/solidity: Solidity, the Contract-Oriented Programming Language)以及以太坊中evm的实现(go-ethereum/core/vm at master · ethereum/go-ethereum · GitHub)。