インライン アセンブリ インライン アセンブリの概要

Solidity でインライン アセンブリに使用される言語は Yulと呼ばれ、Solidity でインライン アセンブリを記述するために使用される言語は Yul です。

インライン アセンブリは、イーサリアム仮想マシンに低レベルでアクセスする方法です。これにより、いくつかの重要な安全機能と Solidity のチェックが回避されます。これを使用することに自信がある場合にのみ、必要なタスクにのみ使用してください。

インライン アセンブリは、EVM にアクセスするための低レベルの方法です。これは、いくつかの重要な Solidity セキュリティ機能とセキュリティ チェックをバイパスします。必要な場合にのみ使用し、確実に使用できます。

よりきめ細かい制御の場合、特にライブラリを作成して言語を強化する場合、仮想マシンに近い言語を活用して、Solidity ステートメントでインライン アセンブリを使用することができます。EVM はスタックベースの仮想マシンであるため、スタック内のスロットのアドレス (ストレージの場所) を正確にアドレス指定し、オペコードがパラメータを受け取るためのスタック内の正しい場所を提供することが困難なことがよくあります。

インライン アセンブリ ブロックは次のようにマークされます。 assembly { ... }

インライン アセンブリ コードは、ローカルの Solidity 変数にアクセスできます。異なるインライン アセンブリ ブロックは名前空間を共有しません。つまり、別のインライン アセンブリ ブロックで定義された Yul 関数を呼び出したり、関数や変数に定義された Yul 変数にアクセスしたりすることはできません。

外部変数、関数、ライブラリへのアクセス

EVM の仕組み

Solidity のようなスマート コントラクト言語は、EVM によって直接実行できません。代わりに、それらは低レベルの命令 (オペコードと呼ばれる) にコンパイルされる必要があります。

オペコード

EVM は内部で、オペコードと呼ばれる一連の命令を使用して特定のタスクを実行します。この記事の執筆時点では、140 個の一意のオペコードがあります。これらのオペコードを組み合わせると、EVM はチューリング完全環境になります。これは、十分なリソースがあれば、EVM は(ほぼ)あらゆるものを計算できることを意味します。オペコードは 1 バイトであるため、最大でも 256 (16²) 個のオペコードしか存在できません。簡単にするために、すべてのオペコードを次のカテゴリに分類できます。

  • スタック操作オペコード (POP、PUSH、DUP、SWAP)
  • 算術/比較/ビットごとのオペコード (ADD、SUB、GT、LT、AND、OR)
  • 環境オペコード (CALLER、CALLVALUE、NUMBER)
  • メモリ操作オペコード (MLOAD、MSTORE、MSTORE8、MSIZE)
  • ストア操作のオペコード (SLOAD、SSTORE)
  • プログラムカウンター関連のオペコード (JUMP、JUMPI、PC、JUMPDEST)
  • 停止オペコード (STOP、RETURN、REVERT、INVALID、SELFDESTRUCT)

バイトコード

オペコードを効率的に保存するために、オペコードはバイトコードとしてエンコードされます。各オペコードには 1 バイトが割り当てられます (たとえば、STOP は 0x00)。次のバイトコードを見てみましょう: 0x6001600101

実行中、バイトコードはそのバイトに分割されます (1 バイトは 2 つの 16 進文字に相当します)。0x60 ~ 0x7f (PUSH1 ~ PUSH32) の範囲のバイトにはプッシュ データ (別個のオペコードとして扱われるのではなく、オペコードに追加する必要がある) が含まれているため、別の方法で処理されます。

最初の命令は 0x60 で、PUSH1 に変換されます。したがって、プッシュ データの長さが 1 バイトであることがわかっているので、次のバイトをスタックに追加します。スタックには 1 つの項目が含まれるようになり、次の命令に進むことができます。0x01 が PUSH 命令の一部であることがわかっているため、次に実行する必要がある命令は、同じデータを持つ別の 0x60 (PUSH1) です。スタックには 2 つの同一のアイテムが含まれています。最後の命令は 0x01 で、ADD に変換されます。この命令はスタックから 2 つの項目をポップし、それらの項目の合計をスタックにプッシュします。スタックには 1 つのエントリ: 0x02 が含まれています。

ソリディティの最適化 - 契約ガス消費量を削減

ソリディティの最適化 - 契約ガス消費量の削減 - 短い本 (必読)

インラインアセンブリの例

次の例では、別のコントラクトのコードにアクセスし、それを bytes 変数にロードするためのライブラリ コードを提供します。これは「プレーンな Solidity」でも、 を使用することで可能です <address>.codeただし、ここで重要なのは、再利用可能なアセンブリ ライブラリがコンパイラを変更せずに Solidity 言語を強化できるということです。

次の例では、別のコントラクトのコードにアクセスし、それらを bytes 変数にロードするためのライブラリ コードを提供します。

もちろん、純粋な Solidity 言語は <address>.code を通じて実装することもできますが、ここでは再利用可能なアセンブリ ライブラリ コードにより、コンパイラのバージョンを変更せずに Solidity コードの再利用性を最大限に高めることができます。

// SPDX ライセンス識別子: GPL-3.0
プラグマ ソリッドティ >=0.4.16 <0.9.0;

ライブラリ GetCode {
    function at(address addr) public view returns (バイトメモリコード) {
        組み立て {
            // コードのサイズを取得します。これにはアセンブリが必要です。 アドレスに従ってコードのサイズを取得します。
            サイズ := extcodesize(addr)
            // 出力バイト配列を割り当てます - これはアセンブリなしでも行うことができます
            // code = new bytes(size) を使用します
            code := mload(0x40) // Ethereum は、次に利用可能なポインター (空きポインター) をアドレス 0x40 に保存しました。

            // 以前に保存されたデータが上書きされないように、次の空きデータ ポインターの位置を 0x40 に更新します。 // 残りのデータ長の保存スペース add(size, 0x20)
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // 長さをメモリに保存する
            mstore(code, size) // code はデータの保存開始位置を表し、mstore(p,v) は m[p..p+32] を表します。 :=v (p+32 は保存スロットを表します。) 一般データ格納開始位置のスロットにはデータの長さが格納されます。

            // 実際にコードを取得します。これにはアセンブリが必要です
            extcodecopy(addr, add(code, 0x20), 0, size) // add(code, 0x20) はコードデータ格納場所の次のスロットを表し、1 スロットは 32 バイトです。
        }
    }
}

// SPDX ライセンス識別子: GPL-3.0
プラグマ ソリッドティ >=0.4.16 <0.9.0;


ライブラリ VectorSum {
    // 現在オプティマイザが次の処理に失敗しているため、この関数は効率が低くなります。
    // 配列アクセスの境界チェックを削除します。
    function sumSolidity(uint[] メモリ データ) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i)
            合計 += データ[i];
    }

    // 範囲内でのみ配列にアクセスすることがわかっているため、チェックを回避できます。
    // 最初のスロットには 0x20 が含まれるため、配列に 0x20 を追加する必要があります。
    // 配列の長さ。
    function sumAsm(uint[] メモリデータ) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i) {
            組み立て {
                sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
            }
        }
    }

    // 上記と同じですが、コード全体をインライン アセンブリ内で実行します。
    function sumPureAsm(uint[] メモリ データ) public pure returns (uint sum) {
        組み立て {
            // 長さ (最初の 32 バイト) をロードします
            let len := mload(data) // data はアセンブリ内の配列値そのものではなく、整数である配列値データストレージの開始位置のみを表します。mload(p) は m(p, p+32 ). データを位置 p から位置 p+32 にロードします。コントラクト ストレージ操作の最小単位はスロットであるため、ストレージ スロットは 32 バイトの配列です。配列の最初のストレージ スロットは、格納された配列。

            // 長さフィールドをスキップします。
            //
            // 一時変数をそのまま保持して、その場でインクリメントできるようにします。
            //
            // 注: データをインクリメントすると使用できなくなる可能性があります
            // このアセンブリ ブロックの後のデータ変数
            let dataElementLocation := add(data, 0x20); //0x20 はストレージ スロットの長さ、32 バイトです。読み取りまたは書き込みもスロットごとの読み取りまたは書き込みです。

            // 境界が満たされなくなるまで繰り返します。
            ために
                { let end := add(dataElementLocation, mul(len, 0x20)) }
                lt(dataElementLocation, end)
                { dataElementLocation := add(dataElementLocation, 0x20) }
            {
                sum := add(sum, mload(dataElementLocation))
            }
        }
    }
}

mload パラメータは操作対象のデータのメモリ格納場所を表し、実際のデータを表す 32 バイトのメモリ スロットが返されます。

mstore(p,v)、p はデータが保存されるメモリ アドレスを表し、v は保存される特定のデータを表します。位置は通常 p+32 バイト、保存用のメモリ スロット、サイズは 32 バイトです。仮想マシンが読み取りおよび書き込みを行う場合、1 つのカード スロットに対して 1 つのカード スロットの読み取りまたは書き込みが行われ、1 つのカード スロットは 32 バイトです。

sload、sstore には自動インクリメント p..p+32 のロジックがありません。

[無題] Solidity メモリ割り当て (mload 0x40)_mload Solidity_h_sn999 のブログ - CSDN ブログ(必読)

[無題] Solidity メモリ割り当て (mload 0x40)_mload Solidity_h_sn999 のブログ - CSDN ブログ  (必読)

イーサリアム契約言語開発の簡単な紹介_クロスチェーン技術実践者のブログ - CSDNブログ

ヒープとスタックの違いは?

コマンドスタイル:

3 0x80 mload 0x80 mstore を追加

機能的なスタイル:

mstore(0x80, add(mload(0x80), 3)) は、  メモリ アドレス 080 のデータをメモリにロードして 3 に加算し、メモリ アドレス 0x80 に格納します。

関数型スタイルのパラメーターの順序は、指示型スタイルのパラメーターの順序とは逆になります。関数スタイルが使用される場合、最初のパラメーターはスタックの一番上にあります。

外部変数、関数、ライブラリへのアクセス

メモリを参照するローカル変数は、値自体ではなく、メモリ内の変数のアドレスとして評価されます。このような変数に代入することもできますが、代入によって変更されるのはデータではなくポインタのみであり、代入を行うのはユーザーの責任であることに注意してください。 Solidity のメモリ管理を尊重します。Solidity の 規則を参照してください。ローカル変数は、値自体ではなく、メモリ内の値のアドレスを指します。したがって、変数を割り当てることができますが、この割り当てでは、データではなく変数参照のアドレスのみが変更されます。メモリ管理はあなたの責任です。

同様に、静的にサイズ設定された calldata 配列または calldata 構造体を参照するローカル変数は、値自体ではなく、calldata 内の変数のアドレスに評価されます。変数には新しいオフセットを割り当てることもできますが、それを保証するための検証は実行されないことに注意してください。同様に、ローカル変数 calldatasize()は静的なサイズの配列または構造体を指し、基本的にデータそのものではなくデータ ストレージ アドレスを参照します。変数に新しいオフセットを割り当てることはできますが、オフセットが範囲外であることを確認するための対応するチェックはありません。

ローカル ストレージ変数または状態変数の場合、単一の完全なストレージ スロットを占有する必要はないため、単一の Yul 識別子では十分ではありません。したがって、それらの「アドレス」はスロットとそのスロット内のバイトオフセットで構成されます。変数が指すスロットを取得するには x、 を使用し x.slot、バイトオフセットを取得するには、 を使用します x.offsetそれ自体を使用すると x エラーが発生します。

ローカル変数または状態変数の場合、単一のストレージ スロットを埋める必要がないため、単一の Yul 識別子では十分ではありません。そのため、それらのストレージ アドレスはスロットとスロット内のバイト オフセットで構成されます。変数 x が指すカード スロットを取得するには、x.slot を使用します。ストレージ オフセットを取得するには、x.offset ストレージ アドレスは、x.slot+x.offset です。

避けるべきこと

インライン アセンブリは非常に高レベルに見えるかもしれませんが、実際には非常に低レベルです。関数呼び出し、ループ、if、スイッチは単純な書き換えルールによって変換され、その後アセンブラが行うことは、関数型のオペコードの再配置、変数アクセスのスタック高さのカウント、アセンブリローカル変数のスタックスロットの削除だけです。ブロックの終わりに達したとき。

インライン アセンブリはかなり高レベルに見えますが、実際には非常に低レベルです。関数呼び出し、ループ、if、スイッチは単純な書き換えルールによって変換されます。その後、アセンブラが行うことは、関数オペコードの再配置、変数アクセスのスタック高の計算、アセンブリ ローカル変数のスタック スロットの削除だけです。

メモリの安全性

インライン アセンブリを使用しない場合、コンパイラはメモリに依存して、常に適切に定義された状態を維持できます。これは、Yul IR を介した新しいコード生成パイプラインに特に関連します 。このコード生成パスは、メモリ使用に関する特定の前提に依存できる場合、スタックが深すぎるエラーを回避し、追加のメモリ最適化を実行するために、ローカル変数をスタックからメモリに移動できます。 。

インライン アセンブリを使用せずに、コンパイラは常にメモリに依存して良好な状態を維持できます。これは、新しいコード生成パイプライン Yul IR に関連しています。Solidity は 2 つの方法でインライン アセンブリ コードを生成します。1 つはコマンド ライン モードで、もう 1 つは IR を使用します。このコード生成パスは、変数をスタックからメモリに移動して、ストックが深すぎるエラーを回避できます。また、特定のメモリ消費量に依存している場合は、追加のメモリ最適化が実行されます。

Solidity のメモリ モデルを常に尊重することをお勧めしますが、インライン アセンブリでは互換性のない方法でメモリを使用することができます。したがって、メモリ操作を含む、またはメモリ内の Solidity 変数に割り当てるインライン アセンブリ ブロックが存在する場合、スタック変数のメモリへの移動と追加のメモリ最適化は、デフォルトでグローバルに無効になります。

Solidity のメモリ モデルに常に従うことをお勧めしますが、インライン アセンブリでは常に互換性のない方法でメモリを使用することができます。したがって、メモリ操作や Solidity 変数への割り当てを含むインライン アセンブリ モジュールの前では、スタック変数のメモリへの移動や追加のメモリ最適化はデフォルトで無効になります。

ただし、次のようにアセンブリ ブロックに具体的に注釈を付けて、実際に Solidity のメモリ モデルを尊重していることを示すことができます。

リミックスで開く

アセンブリ (「メモリセーフ」) {
    ...
}

       

おすすめ

転載: blog.csdn.net/gridlayout/article/details/131294519