Deep EVM #1:EVMがコードを実行する仕組み — オペコード、スタック、ガス
Engineering Team
EVMはスタックマシンである
Ethereum Virtual Machineは、ラップトップのx86プロセッサとは全く異なります。レジスタがありません。代わりに、各要素が256ビット(32バイト)のワードである1024要素のスタックにプッシュまたはポップするスタックマシンです。
スマートコントラクトを呼び出すと、EVMはコントラクトのバイトコード — 単一バイトのオペコードのフラットなシーケンス — を受け取り、バイト0から実行を開始します。関数テーブルも、ELFヘッダーも、リンクステップもありません。バイトコードがプログラムです。
// Solidity:
// uint256 result = 2 + 3;
// バイトコードにコンパイル:
// PUSH1 0x02 PUSH1 0x03 ADD
// スタックトレース:
// [] -> PUSH1 0x02 -> [2]
// [2] -> PUSH1 0x03 -> [2, 3]
// [2, 3] -> ADD -> [5]
すべてのオペコードはスタックの上部からオペランドを消費し、結果をプッシュバックします。ADDオペコードは2つの値をポップし、加算し、合計をプッシュします。
オペコードカテゴリ
EVMは約140のオペコードを定義し、機能カテゴリにグループ化されています:
算術と比較
- ADD、SUB、MUL、DIV、MOD — 基本的な256ビット整数演算。すべて3ガス(G_verylow階層)。
- SDIV、SMOD — 2の補数を使用した符号付き除算と剰余。
- ADDMOD、MULMOD — モジュラー演算:単一のオペコードで
(a + b) % Nと(a * b) % N。楕円曲線演算に不可欠で、8ガス。 - EXP — べき乗。10ガス + 指数のバイトあたり50、最も高価な算術オペコードの1つ。
- LT、GT、SLT、SGT、EQ、ISZERO — 1(true)または0(false)をプッシュする比較オペコード。
ビット演算
- AND、OR、XOR、NOT — ビット論理、各3ガス。
- SHL、SHR、SAR — 左シフト、論理右シフト、算術右シフト(Constantinople、EIP-145で追加)。
- BYTE — 32バイトワードから単一バイトを抽出。
スタック操作
- POP — 先頭要素を破棄。
- PUSH1〜PUSH32 — 1〜32バイトの即値データをスタックにプッシュ。
- DUP1〜DUP16 — N番目のスタック要素を先頭に複製。
- SWAP1〜SWAP16 — 先頭要素をN番目の要素と交換。
ガススケジュール
すべてのオペコードにはガスコストがあります。ガスは2つの目的を果たします:無限ループの防止(停止問題)と計算リソースの公正な価格設定です。
| 階層 | ガス | 例 |
|---|---|---|
| Zero | 0 | STOP、RETURN、REVERT |
| Base | 2 | ADDRESS、ORIGIN、CALLER |
| Very Low | 3 | ADD、SUB、LT、GT、AND、OR、POP |
| Low | 5 | MUL、DIV、MOD |
| Mid | 8 | ADDMOD、MULMOD、JUMP |
| High | 10 | JUMPI |
| Special | 可変 | SLOAD、SSTORE、CALL、CREATE |
高価なオペコードは状態にアクセスするものです:
// 状態アクセスのガスコスト(EIP-2929以降):
// SLOAD(コールド):2100ガス
// SLOAD(ウォーム):100ガス
// SSTORE(コールド、0→非ゼロ):22100ガス
// SSTORE(ウォーム):100ガス(+ 0→非ゼロの場合20000)
// CALL(コールド):2600ガス
// CALL(ウォーム):100ガス
コールド vs ウォームアクセス(EIP-2929)
EIP-2929(ベルリンアップグレード、2021年4月)は、アクセスリストの概念を導入しました — トランザクション内でアクセスされたアドレスとストレージスロットのセットです。
トランザクション内で初めてストレージスロットまたは外部アドレスにアクセスする場合、「コールド」となり追加のガスがかかります。同じアドレス/スロットへのその後のアクセスは「ウォーム」で安価です。ストレージスロットを読む順序がガス最適化に重要な理由がここにあります。
実行フロー:トランザクションで何が起こるか
コントラクトを呼び出すトランザクションを送信すると、以下の完全な実行シーケンスが発生します:
- トランザクション検証 — ナンス確認、残高チェック、署名検証
- 固有ガス控除 — トランザクション自体に21000ガス、非ゼロcalldataバイトあたり16ガス、ゼロバイトあたり4ガス
- コンテキストセットアップ — EVMが実行コンテキストを作成
- プログラムカウンタが0から開始 — EVMがposition 0のオペコードを読み取り実行
- 順次実行 — 各オペコードが実行され、ガスが控除される
- 終了 — STOP、RETURN、REVERT、またはガス切れで実行が終了
- 状態コミットまたはロールバック — 成功時はすべての状態変更がコミット、リバート時はロールバック
プログラムカウンタとJUMP
プログラムカウンタ(PC)はバイトコード内の現在位置を追跡する暗黙のレジスタです。2つのオペコードがPCを直接変更します:
- JUMP — スタックからデスティネーションをポップし、PCをその値に設定。デスティネーションにはJUMPDESTオペコードが必要。
- JUMPI — 条件付きジャンプ。デスティネーションと条件をポップ。条件が非ゼロならジャンプ。
Solidityコンパイラは、calldataの最初の4バイトを読み込み、既知の関数シグネチャと比較し、一致するコードブロックにJUMPIする関数セレクタを生成します。
サブコール:CALL、STATICCALL、DELEGATECALL
コントラクトは3つのコールオペコードで他のコントラクトを呼び出せます:
- CALL — 標準コール。独自のスタックとメモリを持つ新しい実行コンテキストを作成。
- STATICCALL — 読み取り専用コール(EIP-214)。呼び出し先での状態変更オペコードは即座にリバート。
- DELEGATECALL — 呼び出し先のコードを呼び出し元のストレージコンテキストで実行。プロキシパターンの基盤。
MEVへの実践的影響
MEVボットを構築している場合、オペコードレベルでのEVMの理解はオプションではなく、競争上の必要条件です。ボットの実行で節約されるすべてのガスユニットが利益マージンです。
- 送信前にシミュレーション —
eth_callまたはローカルEVM(revm、EVMONE)を使用 - コールドアクセスを最小化 — アクセスリスト(EIP-2930)でストレージスロットをプリウォーム
- 読み取りにはSTATICCALLを使用 — わずかに安価で状態変更がないことを保証
- オペコードコストを把握 — 1つのミスプレースされたSLOADが2100ガスのコスト
まとめ
EVMはそのシンプルさが美しい:256ビットワードのスタックマシン、フラットなバイトコードフォーマット、そしてすべての操作に価格をつけるガスメータリングシステム。この基盤 — オペコード、スタック、ガス — を理解することが、本シリーズで続くすべての前提条件です。