# EIP-4200:EOF - 靜態相對跳躍
# 注意事項
本文並沒有經過作者外的其他審查者審核,因此若內容有誤,請到 issue 區提出問題,我會儘速修改,謝謝。
# 簡單介紹
我們要在 EVM 指令中加入三個新的跳躍指令(RJUMP
、RJUMPI
和 RJUMPV
),用來取代原本的動態 JUMP
系列跳躍指令。這些指令可以告訴程式「跳到前面或後面的某個地方繼續執行」。這些新指令比舊的方法更省 Gas 費用,也更容易使用。
# 為什麼我們需要這個?
想像你在玩遊戲,有時會需要跳過某些關卡,或是回頭重新挑戰先前的部分。在電腦程式中,這種控制流程跳躍是非常常見的需求。
目前的 EVM 只有一種跳躍方式「動態跳躍」,就像是你每次想跳到哪裡都需要先查地圖,確認目的地(JUMPDEST
)才能跳。這種方式很靈活,但也有點浪費時間和資源。
PUSH2 0x1234
JUMP
但這種方式有幾個缺點:
- 每次跳躍都必須事先查找目標位置是否為有效的 JUMPDEST
- JUMPDEST 會消耗額外的 1 gas
- 靜態分析工具不容易預先知道跳躍目標
其實在很多情況下,我們事先就知道要跳到哪裡,不需要每次都查地圖。就像你知道「跳到前面 5 步」或「跳到後面 3 步」這樣簡單的指令就好。
RJUMP -5
這就等於說:「從目前位置向前跳 5 個位元組」,不需要查表也不需要額外標記。
這個改變將提供一個更簡單的選擇。程式設計師可以根據需要選擇最適合的方法。這個新功能可以幫助我們:
- 省 Gas(使用和部署程式時都更便宜)
- 讓程式更容易分析和理解
# 規格說明
我們要引入三個新指令:
RJUMP
(0xe0) - 直接跳躍到指定的相對位置RJUMPI
(0xe1) - 如果條件成立,才跳躍到指定的相對位置RJUMPV
(0xe2) - 根據一個數字選擇從多個跳躍目標中跳到其中一個
如果是傳統 EVM 的程式碼,這些指令會導致程式停止執行,因此不會改變現有程式的行為。
如果是新式 EOFv1 格式的程式碼:
RJUMP relative_offset
會把程式運行位置設定到「目前位置 + relative_offset」。RJUMPI relative_offset
會從堆疊中取出一個值(condition
),如果這個值不是 0,就跳到「目前位置 + relative_offset」;如果是 0,就不跳躍。RJUMPV max_index relative_offset+
會從堆疊中取出一個值(case
),如果這個值小於或等於max_index
,就跳到「目前位置 + relative_offset[case]」;否則不跳躍。
relative_offset
是一個 16 位元的有符號(可正可負)的大端序數值。「目前位置」指的是整個指令(包括參數)之後的位置。
RJUMPV
的參數比較特別:max_index
是一個 8 位元的無符號值,決定跳躍表的最大索引。後面的 relative_offset
值的數量是 max_index+1
。這樣可以有最多 256 個跳躍目標。RJUMPV
至少需要一個 relative_offset
,所以最少會佔用 4 個位元組。
我們也擴展了之前 EIP-3670 的驗證方法,確保每個 RJUMP
/RJUMPI
/RJUMPV
的 relative_offset
都指向一個有效的指令。這表示它不能指向 PUSHn
/RJUMP
/RJUMPI
/RJUMPV
的即時資料,也不能指向程式碼範圍外。它可以指向 JUMPDEST
,但不是必須的。
因為目標位置是預先驗證的,所以這些指令的成本比動態跳躍更低:RJUMP
應該是 2 gas,RJUMPI
和 RJUMPV
應該是 4 gas。
# 為什麼這樣設計?
# 相對定位
我們選擇相對定位是為了支援可重新定位的程式碼。這也意味著程式碼片段可以被插入。在這個 EIP 之前,要達到同樣的目標,程式設計師會使用像 PUSHn PC ADD JUMPI
這樣的技巧。
我們認為相對定位沒有明顯的缺點,而且它允許我們也棄用 PC
指令。
# 參數大小
有符號的 16 位元參數意味著最大跳躍距離可以是 32767。如果在 PC=0
的位置的程式碼以 RJUMP
開始,就可以跳躍到 PC=32770
的位置。
考慮到 MAX_CODE_SIZE = 24576
(在 EIP-170 中)和 MAX_INITCODE_SIZE = 49152
(在 EIP-3860 中),16 位元的參數足夠大了。
# 沒有使用 JUMPDEST
JUMPDEST
有兩個用途:
- 有效地分割程式碼 — 這對於預先計算給定區塊的總 gas 用量很有用
- 明確顯示有效的位置(否則任何非資料位置都將是有效的)
對於靜態跳躍,不需要這個功能,因為分析器可以在分析過程中輕鬆地從靜態跳躍參數中識別目標位置。
這裡有兩個好處:
- 每個跳躍目標不用浪費一個位元組的
JUMPDEST
,也意味著在部署時每個跳躍目標可以節省 200 gas。 - 在執行過程中每次跳躍可以額外節省 1 gas,因為
JUMPDEST
本身花費 1 gas,並且在跳躍過程中被「執行」。
# RJUMPV
的預設情況
如果在 RJUMPV
指令執行中沒有找到匹配項(即預設情況),程式會繼續執行而不跳躍。這允許在參數中的空缺處填入 0
,並且讓程式設計師自己選擇實作方式。
另一種方案是在沒有匹配的情況下異常中止。
# 範例
# RJUMP (0xe0)
直接跳到目前位置 + relative_offset。
# 指令
PC=100:
RJUMP 0x0006 ; 直接跳到 PC=108
... ; 中間這段會被跳過
PC=108:
STOP
# ASCII 圖示:
100 ─┐
│ RJUMP +6
101 │
... │ ← 跳過
108 ┘→ STOP
# RJUMPI (0xe1)
根據條件跳躍。從堆疊中取出 condition,若不為 0,則跳到 PC + relative_offset。
PUSH1 1 ; condition = true
RJUMPI 0x0004 ; 跳到後面 if 區塊
...
STOP
# ASCII 圖示:
堆疊: [1] ← condition = true
│
▼
RJUMPI +4 ───→ if 區塊
│
└──────→ 不跳(若為 0)
# RJUMPV (0xe2)
從堆疊取出 case 數值,跳到對應的 relative_offset[case] 位置。最多支援 256 個跳躍目標。
# 指令
PUSH1 2
RJUMPV 0x02
0x0004 ; case = 0
0x0006 ; case = 1
0x0008 ; case = 2 → 這個會被使用
# ASCII 圖示:
堆疊:[2] → case = 2
RJUMPV max=2:
├─ case 0 → offset +4
├─ case 1 → offset +6
└─ case 2 → offset +8 ← 命中
# 若 case 超過最大值(例如 3 > max_index=2),則不跳躍,繼續執行下一指令。