本篇使用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)
了解RETURN
和MSTORE8
之後,來改寫一下剛剛的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
看起來跟剛剛的MSTORE8
87%像,不過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://github.com/CoinCulture/evm-tools/blob/master/analysis/guide.md