Remixを使ってSolidityのコンパイル結果を追ってみる
Solidityで書かれたコードはコンパイルされてEVM上で実行される。このコンパイルされたコードの結果をRemixを使ってどういった処理が行われるのか追ってみたい。
前提知識
Solidity
Ethereumでスマートコントラクトを書くための言語。この記事を読もうとしてる人なら特に説明は不要そうなので詳細は割愛するがそれだけだとあまりにも雑なのでスマートコントラクト開発に関するコード解説動画をアップロードしているおすすめYoutubeチャンネルだけおいておく。
YouTubeのvideoIDが不正ですEVM
EVMはEthereumのスマートコントラクトの実行やアカウントの状態が保持されている場所である。1命令は32バイトのスタックベースのVM。EVMのOPCODEについては下記のサイトを見るのがわかりやすい。
EVMの公式解説は下記。
あとはこの記事とかを読んでスタックマシンについて学んでおくのも良さげ。
Remix
ところでRemixというのは何かというとEthereum公式のWebIDEだ。Solitidyで書いたコードをブラウザ上で実行してデバッグしたり、オンラインのネットワークへそのままデプロイしたり出来る便利なやつである。
Remixの使い方については公式ドキュメントを読んだりこのチュートリアルをやってみると雰囲気が掴めると思う。
今回使うコード
まぁ何はともあれまずはRemixを開いてコードを書いて実際に動かしてみる方が早いはず。ということで早速コードを書いていく。今回使うのはこのコード。xとyを足し算して返すだけの内容である。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Sample {
function addition(uint x, uint y) public pure returns (uint) {
assembly {
let result := add(x, y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
ただしassembly
に関しては見慣れないと思う。これはSolidityのInline Assemblyという機能で、EVMに対して直接命令を書ける機能だ。 素のSolidityでは手の届かない高速化やガス代の節約などを行いたい時に使われることが多い。
最近OpenSeaで採用されたNFTとトークンを組み合わせて購入出来るようにするためのプロトコルSeaportのコントラクト内でも各所で使われている。気になったら読んでみると良い。
話を戻すが、なぜわざわざassembly
を使ったコードを書いているかというと、その方がコンパイルして生成されるinstructionが簡潔だからである。assembly
を使わずに素のSolidityで書くとコンパイラが最適化をして素直なコードを生成してくれないことがあり処理が追いづらくなるので簡単な処理だがassembly
を使っている。
コンパイル
Remixを開いてサイドバーの1番上のアイコンをクリックするとFILE EXPLORERSというのが開く。すると色々なディレクトリが表示されるのでそのうちのcontracts/
を開き、適当に1_Sample.sol
みたいなファイルを作成する。そして先ほど挙げたコードをペタリと貼り付ける。
次にサイドバーの中段ほどにあるアイコンをクリックしてSOLIDITY COMPILERを開く。Compile 1_SAMPLE.sol みたいなボタンが表示されていると思うのでこれをクリックする。するとコンパイルが即座に行われる。
デプロイ
コンパイルが終わったので実際に生成されたコードを追ってみたいのだが、それにはまずはデプロイする必要がある。Remixではブラウザ上にコードをデプロイして実行する機能があるのでそれを使ってコードを実行出来る。
まずはサイドバーの先ほどのSOLIDITY COMPILERの下のアイコンをクリックする。するとDEPLOY&RUN TRANSACTIONSというペインが開く。ENVIRONMENTが Javascript VM(London) でCONTRACTが Sample contracts/1_Sample.sol となっているか確認し、deployボタンを押す。
これでローカルにデプロイが完了した。あとは実行してみるだけである。
デバッグ
早速デプロイしたコントラクトを実行してみる。先ほどのDEPLOY&RUN TRANSACTIONSのdeployボタンの下の方にDeployed Contractsというのがあるのでそこを開くとSample
コントラクトで定義したaddition
という関数が実行できるようになっているのがわかる。とりあえずここにxを1, yを2と入力してcallボタンを押してみる。すると即座に実行される。
ちょっとわかりにくいのだけど上矢印を二つ重ねたようなアイコンをクリックすると上記画像のように実行結果を確認できるTerminalのようなものが表示される。
ここにDebugというボタンが表示されていると思う。これを押すとコンパイルされたコードのInstructionが左ペインに表示されるようになる。これで命令単位でステップ実行したりできる。
実際に処理を追う
左ペインにはFunction StackやらSolidity Stateやら色々と表示されているが実際に今回着目するのはPUSH1
やADD
などのInstruction表示部分とその下の方にあるStackとMemoryだけ。あとは無視してok。
まずは見づらいので今回生成されたaddition
の処理に関連するInstructionを抜粋すると下記だけである。
PUSH1 00
DUP2
DUP4
ADD
DUP1
PUSH1 00
MSTORE
PUSH1 20
PUSH1 00
RETURN
そしてStackの状態はこんな感じ。要はadditionの引数のxに指定した1と2である。
0x0000000000000000000000000000000000000000000000000000000000000002
0x0000000000000000000000000000000000000000000000000000000000000001
この状態からスタートしてInstructionsのPUSH1 00
から一行ずつ順に実行されていく過程で1+2=3という計算がどのように行われていくのか。それを一つずつ追っていく。ちなみにStackの状態も併記するが簡便のため数字は0x01
と0x02
と2桁までで表記する。
PUSH1 00
PUSH1 X
は1byteのデータをStackの先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x00
1: 0x02
2: 0x01
DUP2
DUP2
はStackの2byte目のデータ(今回の場合は0x02が2byte目。0x00が1byte目なので注意。)を先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x02
1: 0x00
2: 0x02
3: 0x01
DUP4
DUP4
はStackの4byte目のデータを先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x01
1: 0x02
2: 0x00
3: 0x02
4: 0x01
ADD
ADD
はStackの0,1番目を足して1番上のStackに積む。よってこれを実行するとSTACKは下記のようになる。
0: 0x03
1: 0x00
2: 0x02
3: 0x01
DUP1
DUP1
はStackの1byte目のデータを先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x03
1: 0x03
2: 0x00
3: 0x02
4: 0x01
PUSH1 00
PUSH1 X
は1byteのデータをStackの先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x00
1: 0x03
2: 0x03
3: 0x00
4: 0x02
5: 0x01
MSTORE
MSTORE
はStackの0番目の値(0x00)の位置+32をした位置(=つまり16進表記だと0x10のMemoryの位置)にStackの1番目の値(0x03)を保存して、Stackの0番目,1番目の値を取り除く。つまりこれを実行するとSTACKは下記のようになる。
0: 0x03
1: 0x00
2: 0x02
3: 0x01
そしてMEMORYの状態は下記のようになる。
0x00: 0x00
0x10: 0x03
PUSH1 20
PUSH1 X
は1byteのデータをStackの先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x20
1: 0x03
2: 0x00
3: 0x02
4: 0x01
PUSH1 00
PUSH1 X
は1byteのデータをStackの先頭に積む命令。よってこれを実行するとSTACKは下記のようになる。
0: 0x00
1: 0x20
2: 0x03
3: 0x00
4: 0x02
5: 0x01
RETURN
RETURN
はStackの0番目の値(0x00)の位置から1番目の値(0x20)のサイズ(=つまり0x00から0x20なので32byte) 分のMemoryの値を返して実行を終える。現在のMemoryの状態は下記なのでつまり0x03(1+2の結果)を返す。
0x00: 0x00
0x10: 0x03
まとめ
Solidityで書いた簡単なコードをRemixのデバッグ機能を使って1行ずつ追ってみた。
今回は単純な関数でローカル変数と引数を使った内部処理しかないからStackとMemoryを使うだけだったが、EVMにはその他にもStorage領域やCalldata領域やReturnData領域などの5つのデータ領域が存在する。特にStorageはブロックチェーンにデータを永続化するために使う領域でガス代に大きく影響を及ぼす領域なので押さえておきたい。この辺が初心者向けにまとまっている記事は多くないので少し古いが下記の記事はわかりやすいと思う。
Solidityで生成されたバイトコードもこのようにRemixを使って処理を1stepずつ追っていくと実際に何が起こっているのか理解しやすかったのではないだろうか。実際にもう少し複雑なコードを書くと生成されるバイトコードの量も格段に増えるのでこんなに簡潔な命令数になることはないが基本的なデバッグ方法としては変わらないと思う。
次のステップとしては自分なりに適当なコードを書いて実行してみて、今回やったようにInstructionsを一つずつ追っていくとEVMへの理解が進むだろう。
関連リンク
YouTubeのvideoIDが不正です
Discussion