本篇使用go-ethereum Version: 1.9.23-stable

概述

EVM跑起來就像是一個stack machine,stack整體是 1024 * 256 bit 個空間。

運作期間可以對Storage或是Memory做儲存或是讀取的操作。

至於各個OPCODE的操作可以搭配參照這裡

1 + 1

首先我們來執行一個小小的程式 1 + 1

❯ evm --code 6001600101 --debug run
0x
#### TRACE ####
PUSH1           pc=00000000 gas=10000000000 cost=3

PUSH1           pc=00000002 gas=9999999997 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001

ADD             pc=00000004 gas=9999999994 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
00000001  0000000000000000000000000000000000000000000000000000000000000001

STOP            pc=00000005 gas=9999999991 cost=0
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000002

#### LOGS ####

這邊的code可以拆成三個部分來看,分別是6001 6001 01

所對應到的指令會像是這樣

00000: PUSH1 0x01  // PUSH 1 byte長度的數值 0x01
00002: PUSH1 0x01  // PUSH 1 byte長度的數值 0x01
00004: ADD         // 相加Stack最上面的兩個元素後,把結果放在頂端

如上面的debug最後一個步驟,我們可以看到0x02就在stack的頂端。

PUSH系列的指令在evm裡面共用32種,分別就是1~32,而數字所對應的就是長度(單位是byte)。

我們可以改一下上面的範例,換成是257 + 257

❯ evm --code 61010161010101 --debug run
0x
#### TRACE ####
PUSH2           pc=00000000 gas=10000000000 cost=3

PUSH2           pc=00000003 gas=9999999997 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000101

ADD             pc=00000006 gas=9999999994 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000101
00000001  0000000000000000000000000000000000000000000000000000000000000101

STOP            pc=00000007 gas=9999999991 cost=0
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000202

#### LOGS ####

這邊的話就要改成使用PUSH2的指令了,因為257超過了1byte。

一樣程式碼可以切為三個部分,610101 610101 01

00000: PUSH2 0x0101
00003: PUSH2 0x0101
00006: ADD

運行的結果跟剛剛的1+1一樣,會躺在stack的頂端。至於要怎麼把它拿出來,我們會需要搭配其他的opcode。

Return From Memory

RETURN

[stack]

 -------------------------
| offset | length | ...
 -------------------------

[desc]

return memory[offset:offset+length]

由於是stack的關係,記得length要先push,再來才是offset。另外return是從memory拉東西出來,所以會需要另外一個opcode MSTORE8

MSTORE8

[stack]

 -------------------------
| offset | value | ...
 -------------------------

[desc]

memory[offset] = value & 0xFF

(0xFF是為了確保存進去的東西只保留1 byte。例如value是0x1C01,做 0x1C01 & 0xFF 後只會保留後面的 0x01)

了解RETURNMSTORE8之後,來改寫一下剛剛的1+1

❯ evm --code 600160010160005360016000F3 --debug run
0x02
#### TRACE ####
PUSH1           pc=00000000 gas=10000000000 cost=3

PUSH1           pc=00000002 gas=9999999997 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001

ADD             pc=00000004 gas=9999999994 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
00000001  0000000000000000000000000000000000000000000000000000000000000001

PUSH1           pc=00000005 gas=9999999991 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000002

MSTORE8         pc=00000007 gas=9999999988 cost=6
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  0000000000000000000000000000000000000000000000000000000000000002
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

PUSH1           pc=00000008 gas=9999999982 cost=3
Memory:
00000000  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

PUSH1           pc=00000010 gas=9999999979 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

RETURN          pc=00000012 gas=9999999976 cost=0
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

#### LOGS ####

程式碼步驟可以拆成這樣 6001 6001 01 6000 53 6001 6000 F3,對應到opcode如下

00000: PUSH1 0x01   // PUSH 1 byte資料 0x01
00002: PUSH1 0x01   // PUSH 1 byte資料 0x01
00004: ADD          // 相加頂端兩個元素
00005: PUSH1 0x00   // 推一個0x00進去,這邊的0x00是要存到記憶體的offset,此時的stack會像是 頂端 => 0x00 => 0x02 (0x02是剛剛相加的結果) => 底部
00007: MSTORE8      // 根據opcode定義,會把 0x02 存放到 0x00 的位置
00008: PUSH1 0x01   // 0x01是要從記憶體拉出來的資料長度
0000a: PUSH1 0x00   // 0x00是資料的起點
0000c: RETURN       // 根據opcode定義,會從0x00拉長度為0x01的資料出來,就是我們剛剛放進去的0x02

存資料到memory其實不只有MSTORE8,還可以用另外一個opcode MSTORE

MSTORE

[stack]

 -------------------------
| offset | value | ...
 -------------------------

[desc]

memory[offset:offset+32] = value

看起來跟剛剛的MSTORE887%像,不過MSTORE一次就是存32byte的資料,且EVM存放的時候是Big Endian,所以要特別注意一下存放資料的位置。

再來拿剛剛的1+1程式開刀

❯ evm --code 60016001016000526001601fF3 --debug run
0x02
#### TRACE ####
PUSH1           pc=00000000 gas=10000000000 cost=3

PUSH1           pc=00000002 gas=9999999997 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001

ADD             pc=00000004 gas=9999999994 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
00000001  0000000000000000000000000000000000000000000000000000000000000001

PUSH1           pc=00000005 gas=9999999991 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000002

MSTORE          pc=00000007 gas=9999999988 cost=6
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  0000000000000000000000000000000000000000000000000000000000000002
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

PUSH1           pc=00000008 gas=9999999982 cost=3
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 02  |................|

PUSH1           pc=00000010 gas=9999999979 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 02  |................|

RETURN          pc=00000012 gas=9999999976 cost=0
Stack:
00000000  000000000000000000000000000000000000000000000000000000000000001f
00000001  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 02  |................|

#### LOGS ####
00000: PUSH1 0x01
00002: PUSH1 0x01
00004: ADD
00005: PUSH1 0x00
00007: MSTORE      // 這邊可以搭配上面的記憶體位置看一下,因為是大尾續,所以02會在最後一個位置(0x1f)
00008: PUSH1 0x01
0000a: PUSH1 0x1f
0000c: RETURN

總結

EVM的操作基本上都是在stack裡面完成的,由於已經是底層了,所以資料的長度和擺放的順序都非常的重要。剛剛上述的範例都很建議可以搭配參照debug來看, debug資訊裡面有evm目前上面資料的狀態,而且畫得非常清楚(σ゚∀゚)σ

Ref:

https://ethervm.io/

https://github.com/CoinCulture/evm-tools/blob/master/analysis/guide.md

https://en.wikipedia.org/wiki/Endianness