🐼

Remixを使ってSolidityのコンパイル結果を追ってみる

2022/05/31に公開

Solidityで書かれたコードはコンパイルされてEVM上で実行される。このコンパイルされたコードの結果をRemixを使ってどういった処理が行われるのか追ってみたい。

前提知識

Solidity

Ethereumでスマートコントラクトを書くための言語。この記事を読もうとしてる人なら特に説明は不要そうなので詳細は割愛するがそれだけだとあまりにも雑なのでスマートコントラクト開発に関するコード解説動画をアップロードしているおすすめYoutubeチャンネルだけおいておく。
YouTubeのvideoIDが不正ですhttps://www.youtube.com/channel/UCJWh7F3AFyQ_x01VKzr9eyA

EVM

EVMはEthereumのスマートコントラクトの実行やアカウントの状態が保持されている場所である。1命令は32バイトのスタックベースのVM。EVMのOPCODEについては下記のサイトを見るのがわかりやすい。
https://www.evm.codes/

EVMの公式解説は下記。
https://ethereum.org/en/developers/docs/evm/

あとはこの記事とかを読んでスタックマシンについて学んでおくのも良さげ。
https://igor.io/2013/08/28/stack-machines-fundamentals.html

Remix

ところでRemixというのは何かというとEthereum公式のWebIDEだ。Solitidyで書いたコードをブラウザ上で実行してデバッグしたり、オンラインのネットワークへそのままデプロイしたり出来る便利なやつである。
https://remix-alpha.ethereum.org/

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のコントラクト内でも各所で使われている。気になったら読んでみると良い。
https://github.com/ProjectOpenSea/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やら色々と表示されているが実際に今回着目するのはPUSH1ADDなどのInstruction表示部分とその下の方にあるStackMemoryだけ。あとは無視して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の状態も併記するが簡便のため数字は0x010x02と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はブロックチェーンにデータを永続化するために使う領域でガス代に大きく影響を及ぼす領域なので押さえておきたい。この辺が初心者向けにまとまっている記事は多くないので少し古いが下記の記事はわかりやすいと思う。

https://y-nakajo.hatenablog.com/entry/2018/06/03/165658

Solidityで生成されたバイトコードもこのようにRemixを使って処理を1stepずつ追っていくと実際に何が起こっているのか理解しやすかったのではないだろうか。実際にもう少し複雑なコードを書くと生成されるバイトコードの量も格段に増えるのでこんなに簡潔な命令数になることはないが基本的なデバッグ方法としては変わらないと思う。

次のステップとしては自分なりに適当なコードを書いて実行してみて、今回やったようにInstructionsを一つずつ追っていくとEVMへの理解が進むだろう。

関連リンク

YouTubeのvideoIDが不正ですhttps://www.youtube.com/channel/UCJWh7F3AFyQ_x01VKzr9eyA
https://www.evm.codes/
https://ethereum.org/en/developers/docs/evm/
https://igor.io/2013/08/28/stack-machines-fundamentals.html
https://remix-alpha.ethereum.org/
https://solidity-jp.readthedocs.io/ja/latest/assembly.html
https://github.com/ProjectOpenSea/seaport
https://y-nakajo.hatenablog.com/entry/2018/06/03/165658
https://ethdebug.github.io/solidity-data-representation/

Discussion