
この記事は一連の記事の第 2 部です。
この記事の序文をまだ読んでいない場合は、まず読んでください。パート 1 はじめに
単純な Solidity スマート コントラクトの EVM バイトコードを分解しています。
今日は、「分割統治」戦略を使用して、スマート コントラクトの複雑なコードを分解してみましょう。序文で述べたように、この逆アセンブルされたコードは実際には非常に低レベルですが、元のバイトコードと比較すると比較的読みやすいでしょう。
序文で紹介した操作に従い、BasicToken コードをリミックス コンパイラーにデプロイしたことを確認してください。
免責事項: この記事で提供されるすべての手順は、トランザクションがどのように機能するかについての私自身の解釈の対象であり、イーサリアムの公式見解を表すものではありません。
今、させてJUMP、JUMPI、JUMPDES、RETURN、STOP 操作に重点を置き、その他の操作はすべて無視します。これらのいずれでもないオペコードを見つけた場合は、それを無視して、干渉せずに次の命令にスキップします。
EVM がコードを実行するときは、上から下の順序で実行され、コード内に他のエントリ ポイントはなく、実行は常に上から開始されます。 JUMP と JUMPI はコードをジャンプさせることができます。 JUMP はスタックの最上位の値を取得し、その位置に移動した命令を実行します。ただし、ターゲットの場所には JUMPDEST オペコードが含まれている必要があります。そうでない場合、実行は失敗します。この唯一の目的は、JUMPDEST がその位置を有効なジャンプ ターゲットとしてマークすることです。 JUMPI もまったく同じですが、スタックの 2 番目の位置に「0」があってはなりません。そうでない場合、ジャンプは行われません。したがって、これは条件付きジャンプです。STOPはスマートコントラクトを完全に停止する命令です。RETURNはスマートコントラクトの実行を一時停止しますが、EVMメモリ内のデータの一部を返します。これは非常に便利です。
それでは、これらすべてを念頭に置いてコードの説明を始めましょう。 Remix のデバッガーで、「トランザクション」のスライダーを左端までスライドさせます。 [Step Into] ボタン (小さな下矢印のように見えます) を使用して、指示に従ってください。
前の命令は無視して、11 番目の命令に直接進み、最初の JUMPI が見つかりました。ジャンプしない場合は、命令 12 ~ 15 を続行し、最終的に REVERT に入り、実行が停止します。ただし、ジャンプした場合、それらの命令は位置 16 (命令 8 でスタックにプッシュされる 16 進数 0x0010) にスキップされます。命令 16 は JUMPDEST です。
「トランザクション」スライダーが一番右に移動するまで、オペコードをステップ実行し続けます。いろいろなことが起こりましたが、RETURN オペコードは位置 68 でのみ見つかります (念のため、STOP 命令 69 のオペコードも)。これはとても不思議なことです。考えてみると、このスマート コントラクトの制御フローは常に命令 15 または 68 で終了します。完了したばかりで、他に考えられるフローはないと判断しました。残りの指示は何でしょうか? ([ディレクティブ] パネルをスワイプすると、コードが 566 位で終了していることがわかります)。
今説明した一連の命令 (0 ~ 69) は、コントラクトの「作成コード」と呼ばれるものです。これはスマート コントラクト コード自体の一部になることはありませんが、スマート コントラクトを作成したトランザクション中に EVM によって 1 回だけ実行されます。すぐにわかりますが、このコードは、作成されたコントラクトの初期状態を設定し、そのランタイム コードのコピーを返す役割を果たします。残りの 497 命令 (70 ~ 566)、ご覧のとおり、実行フローは決して到達せず、デプロイされたスマート コントラクトの一部となるのはこのコードです。。
最初のレベルのタイトル
セクションを作成する
次に、コードの作成部分を詳しく見ていきます。
図 1. BasicToken.sol の作成時の EVM バイトコードの分解
これは、この記事で理解する必要がある最も重要な概念です。作成コードはトランザクションで実行され、スマート コントラクトの実際のコードであるランタイム コードのコピーが返されます。後で説明するように、コンストラクターは実行時コードではなく、作成コードの一部です。スマート コントラクトのコンストラクターは作成されたコードの一部であり、一度デプロイされると、スマート コントラクトのコードには表示されません。
この魔法はどのようにして起こるのでしょうか?これをこれから段階的に分析していきます。
わかりましたしたがって、ここでの問題は、作成時のコードに対応するこれらの 70 の命令を理解することになります。
トップダウンのアプローチに戻り、今度は指示をスキップするのではなく、すべての指示を理解します。まず、PUSH1 および MSTORE オペコードを使用する命令 0 ~ 2 に注目してみましょう。
図 2. 空きメモリ ポインタの EVM バイトコード構造
PUSH1 はスタックの先頭に 1 バイトをプッシュするだけですが、MSTORE はスタックから最後の 2 つの項目を取得し、そのうちの 1 つをメモリに保存します。
mstore(0x40, 0x80)
| |
| What to store.
Where to store.
(in memory)
注: 上記のコード スニペットは Yul っぽいコードです。スタックの要素を左から右に消費し、常にスタックの一番上の要素を最初に消費することに注目してください。
ここでは、数値 0x80 (10 進数 128) が位置 0x40 (10 進数 64) に格納されます。
今議論している件は放っておいてください、何か理由があるなら後で説明します。
次に、Remix の [デバッガー] タブで [スタック] パネルと [メモリ] パネルを開いて、これらの手順を実行しながら視覚化できるようにします。
手順 1 と 3 はどうなったのかと疑問に思われるかもしれません。 PUSH は、2 バイト以上で構成される唯一の EVM 命令です。したがって、PUSH 80 は 2 つの命令です。そこで謎が解けました。命令 1 は 0x80、命令 3 は 0x40 です。
次に5から15までの手順を説明します。
図 3. 非支払小切手 EVM バイトコード構造。
ここにも、CALLVALUE、DUP1、ISZERO、PUSH2、REVERT などの新しいオペコードが多数あります。 CALLVALUE は作成トランザクションに関与する wei の数をプッシュし、DUP1 はスタックの最初の要素をコピーし、ISZERO はスタックの最高値が 0 の場合に 1 をスタックにプッシュします。PUSH2 は PUSH1 と似ていますが、2 バイトをスタックにプッシュします。 REVERT は実行を停止します。
それで、ここで何が起こっているのでしょうか? Solidity では、このアセンブリを次のように記述できます。
if(msg.value!= 0)revert();
このコードは実際にはオリジナルの Solidity ソースの一部ではありませんが、コンストラクターを支払い可能として宣言しなかったため、コンパイラーによって挿入されました。 Solidity の最近のバージョンでは、支払い可能と明示的に宣言されていない機能はイーサを受け取ることができません。アセンブリ コードに戻ると、命令 11 の JUMPI は命令 12 ~ 15 をスキップするか、関連付けられたイーサがない場合は 16 にジャンプします。それ以外の場合、REVERT は 2 つの引数を 0 として実行します (有用なデータは返されないことを意味します)。
わかりました!コーヒーを飲みながら休憩しましょう。
(次の部分は少し難しいので、数分間休憩を取ることをお勧めします。再び集中力に戻る前に、おいしいコーヒーを一杯飲んでください。これまで見てきたことを必ず理解してください。次の部分は少し複雑になるためです。)
完了した内容を視覚化する別の方法が必要な場合は、私が作成したこのシンプルなツール solmap を試してください。これにより、Solidity コードをオンザフライでコンパイルし、EVM オペコードをクリックして関連する Solidity コードをハイライト表示できます。逆アセンブリは Remix とは少し異なりますが、比較すると理解できるはずです。
コーヒータイムですよ!
次に進む準備はできていますか?次は命令 16 ~ 37 です。引き続き Remix のデバッガーを使用してください。 (リミックスはあなたの親友であることを忘れないでください^^)。
図 4. スマート コントラクトのバイトコードの最後に追加されたコードからコンストラクター パラメーターを取得するための EVM バイトコード構造
最初の 4 つの命令 (17 ~ 20) は、メモリ内の位置 0x40 にあるものをすべて読み取り、スタックにプッシュします。思い出せれば、それは 0x80 という数字であるはずです。次の例では、0x20 (10 進数 32) をスタックにプッシュし (命令 21)、その値をコピーし (命令 23)、0x0217 (10 進数 535) をプッシュし (命令 24)、最後に 4 番目の値をコピーします (命令 27)。 0x80。
このような EVM 命令を見ていると、何が起こっているのかしばらくわからなくなってしまっても問題ありません。心配しないでください、それは時々頭に浮かびます。
命令 28 で CODECOPY が実行されます。これは、コピーされたコードが保存されるターゲット メモリの場所、コピー元の命令番号、およびコピーするコードのバイト数の 3 つの引数を取ります。したがって、この場合、0x80 はコード内のバイト位置 (535、32 バイトのコード長のターゲット位置) から始まります。
逆アセンブリ コード全体を見ると、566 個の命令があります。このコードがコードの最後の 32 バイトをコピーしようとするのはなぜですか?実際、パラメータ化されたコンストラクタを含むコントラクトをデプロイする場合、パラメータは生の 16 進データとしてコードの末尾に追加されます (これを確認するには、[説明] パネルを下にスクロールします)。この場合、コンストラクターは uint256 パラメーターを受け取るため、このコードが行うことは、コードの最後に追加された値からパラメーターをメモリーにコピーすることだけです。
これら 32 の命令は、逆アセンブルされたコードとしては意味がありませんが、生の 16 進数で表されます: 0x0000000000000000000000000...0000000000000000000002710。もちろん、これはスマート コントラクトをデプロイするときにコンストラクターに渡した 10 進数値 10000 です。
Remix でこの部分を段階的に繰り返して、何が起こったのかを確実に理解することができます。最終結果は位置 0x00..002710 になるはずです。メモリ内の数値 0x80 を参照してください。
さて、次のパートを始める前に、ウィスキーで休憩を取ることをお勧めします。
ウィスキーアワー!
ウィスキーを一杯飲むことをお勧めします。ここからはすべて下り坂だからです。
次の命令セットは 29 ~ 35 で、メモリ アドレス 0x40 の値 0x80 を値 0xa0 に更新します。ご覧のとおり、値を 0x20 (32) バイトオフセットします。
これで、命令 0 から 2 を理解できるようになります。 Solidity は、「ヌル ポインター」と呼ばれるものを追跡します。これは、何かを保存できるメモリー内の場所であり、(間違えない限り) 誰も上書きしないことが保証されています。したがって、数値 10000 を古い空きメモリの場所に保存したため、空きメモリ ポインタを 32 バイト進めて更新します。
熟練した Solidity 開発者でさえ、「空きメモリ ポインタ」またはコード mload(0x40, 0x80) を見ると混乱します。これは単に「新しいエントリを書き込むときは常に、この時点から開始します。メモリへの書き込みを開始し、オフセットを維持します」と言うだけです。記録"。
Solidity のすべての関数は、EVM バイトコードにコンパイルされると、このポインタを初期化します。
メモリの 0x00 から 0x40 の間に何があるかは、わからないかもしれません。いいえ。 Solidity によって予約されたメモリのセクション。ハッシュ値を計算します。すぐに説明するように、マップやその他の種類の動的データに必要です。
ここで、命令 37 で、MLOAD はメモリから位置 0x40 を読み取り、基本的に値 10000 をメモリからスタックにダウンロードします。この値は新しい値となり、次の命令セットで使用できるようになります。
これは、Solidity によって生成される EVM バイトコードの一般的なパターンです。関数本体が実行される前に、関数のパラメーターが (可能な限り) スタックにロードされ、今後のコードで使用できるようになります。これがまさに次に起こることです。が起きた。
38 から 55 までの説明を続けます。
図 5. コンストラクターのメイン EVM コード。
これらの命令はコンストラクターの本体、つまり Solidity コードにすぎません。
totalSupply_ = _initialSupply;
balances[msg.sender] = _initialSupply;
最初の 4 つの命令 (38 ~ 42) は非常に明白で、最初に 0 がスタックにプッシュされ、次にスタック上の 2 番目の項目がコピーされ (これは 10000 という数値です)、次に数値 0 がコピーされてスタックにプッシュされます。スタック、これはストレージ内のロケーション スロット totalSupply_ です。 SSTORE はこれらの値を使用し、将来の使用に備えて 10000 未満に保つことができるようになりました。
sstore(0x00, 0x2710)
| |
| What to store.
Where to store.
(in storage)
見て!数値 10000 を変数 totalSupply_ に保存します。すごいじゃないですか?
Remix の [デバッガー] タブでこの値を必ず視覚化してください。ストアのフルロードパネルで見つけることができます。
次の命令セット (43 ~ 54) は少し複雑ですが、基本的には 10000 のキー msg.sender を残高マップに保存することを扱います。続行する前に、マップをメモリ内に保持する方法を説明する、Solidity ドキュメントのこの部分を必ず理解してください。
簡単に言うと、マップされた値のスロット (この場合、スマート コントラクトで宣言された 2 番目の変数であるため、数値 1) を、使用されるキー (この場合は、CALLER を取得するためのオペコードを介して msg.sender) に接続します。次に、SHA3 オペコードを含むダイジェストを取得し、それをメモリ内の宛先として使用します。結局のところ、ストレージは単純な辞書またはハッシュ テーブルにすぎません。
命令 43 ~ 45 を続けて、msg.sender アドレスがメモリ (今回は位置 0x00) に格納され、次に命令 46 ~ 50 で値 1 (マップされたスロット) がメモリ位置 0x20 に格納されます。最後に、SHA3 オペコードは、メモリ内の位置 0x00 から位置 0x40 までのすべての Keccak256 ハッシュを計算します。つまり、マップされたスロット/位置と使用されるキーの連結です。これはまさにマップ内で値 10000 が保存される場所です。
sstore(hash..., 0x2710)
| |
| What to store.
Where to store.
この時点で、コンストラクターの本体は完全に実行されています。
最初はこれらすべてが少し圧倒されるかもしれませんが、Solidity での作業の基本的な部分です。理解できない場合は、スタックとメモリのパネルを維持しながら、Remix のデバッガーを数回実行することをお勧めします。
また、下記のようなご質問もお気軽にどうぞ。このパターンは、Solidity によって生成される EVM バイトコードでよく使用されており、すぐに簡単に認識できるようになります。最終的には、マップの特定のキーの値をメモリ内のどこに保持するかを計算するだけです。
図 6. ランタイムコードのレプリケーション構造
命令 56 から 65 では、コードの複製を再度実行します。今回のみ、コードの最後の 32 バイトをメモリにコピーせず、位置 0x0046 (10 進数 70) から始まる 0x01d1 (10 進数 465) バイトをメモリの位置 0 にコピーします。これは複製するコードの膨大な量です。
もう一度スライダーを右端までスライドすると、位置 70 がビルド時 EVM コードの直後で、実行が停止することがわかります。ランタイム バイトコードは、これらの 465 バイト内に含まれます。これは、スマート コントラクトの実行時コードとしてブロックチェーンに保存されるコードの一部です。、コードは、誰かまたは何かがスマート コントラクトと対話するたびに実行されるコードになります。(ランタイム コードについては、このシリーズの後半で説明します)。
それはまさに命令 66 から 69 が行うことです。つまり、メモリにコピーしたコードを返します。
図 7. ランタイム コードは EVM バイトコード構造を返します。
RETURN はメモリにコピーされたコードを取得し、EVM に渡します。この作成コードが 0x0 アドレスへのトランザクションのコンテキストで実行されると、EVM はコードを実行し、戻り値を作成されたスマート コントラクトのランタイム コードとして保存します。
ここまでで、BasicToken コードはスマート コントラクト インスタンスを作成してデプロイし、初期状態とランタイム コードで使用できるようになります。一歩下がって図 2 を見ると、紫色で強調表示された構造を除いて、分析したすべての EVM バイトコード構造が汎用であることがわかります。つまり、バイトコードの作成時に Solidity コンパイラーによって生成されます。 。コンストラクターとコンストラクターの違いは、紫色の部分 (コンストラクターの実際の本体) のみです。バイトコードの最後に埋め込まれたパラメータを受け取り、ランタイム コードをコピーしてそれを返す構造は、ボイラープレート コードおよび汎用 EVM オペコード構造と考えることができます。これで、任意のコンストラクターを表示できるようになり、指示に従う前にコンストラクターを構成するコンポーネントについての一般的な概念が得られるはずです。
このシリーズの次回の投稿では、さまざまなエントリ ポイントでスマート コントラクトの EVM コードを操作する方法から始めて、実際のランタイム コードについて説明します。さて、シリーズの最も難しい部分を消化したばかりなので、当然のことだと自分自身を褒めてあげてください。また、EVM バイトコードの読み取りとデバッグ、一般的な構造の理解、そして最も重要なことに、ビルド時と実行時の EVM バイトコードの違いを理解するための優れた能力も必要です。これが、Solidity においてコントラクト コンストラクターを特別なものにしている理由です。
Cheetah ブロックチェーン セキュリティは、Kingsoft Internet Security のテクノロジーに基づいており、人工知能、NLP、その他のテクノロジーと組み合わせて、ブロックチェーン ユーザーに契約監査やセンチメント分析などのエコロジカル セキュリティ サービスを提供します。
*この記事は、Alejandro Santander によって媒体上で最初に公開され、Cheetah Blockchain によって翻訳および整理されました*
Cheetah ブロックチェーン セキュリティは、Kingsoft Internet Security のテクノロジーに基づいており、人工知能、NLP、その他のテクノロジーと組み合わせて、ブロックチェーン ユーザーに契約監査やセンチメント分析などのエコロジカル セキュリティ サービスを提供します。
レーティングトークン公式サイト https://www.ratingtoken.net/?from=z