インラインアセンブリ
Ethereum Virtual Machineの言語に近い言語で、Solidityの文にインラインアセンブリを挟むことができます。 これにより、より細かな制御が可能となり、特にライブラリを作成して言語を拡張したり、ガス使用量を最適化したりする際に役立ちます。
Solidityのインラインアセンブリに使用される言語は Yul と呼ばれ、詳細はそのセクションに書かれています。 このセクションでは、インラインアセンブリのコードが周囲のSolidityコードとどのように連携するかについてのみ説明します。
警告
インラインアセンブリは、Ethereum Virtual Machineに低レベルでアクセスする方法です。 これは、Solidityのいくつかの重要な安全機能とチェックをバイパスします。 必要なタスクにのみ使用し、使用に自信がある場合のみ使用してください。
インラインアセンブリブロックは assembly { ... } で示され、中括弧内のコードは Yul 言語のコードです。
インラインのアセンブリコードは、以下のようにSolidityのローカル変数にアクセスできます。
異なるインラインアセンブリブロックは、名前空間を共有しません。 つまり、異なるインラインアセンブリブロックで定義されたYul関数を呼び出したり、Yul変数にアクセスしたりできません。
例
次の例では、他のコントラクトのコードにアクセスし、それを bytes 変数にロードするライブラリコードを提供しています。
これは「素のSolidity」でも <address>.code を使えば可能です。
しかし、ここでのポイントは、再利用可能なアセンブリライブラリは、コンパイラを変更することなくSolidity言語を強化できるということです。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library GetCode {
function at(address addr) public view returns (bytes memory code) {
assembly {
// コードのサイズを取得します。これはアセンブリが必要です。
let size := extcodesize(addr)
// 出力バイト配列を確保します。
// これは、code = new bytes(size) を用いて、アセンブリなしで行うこともできます。
code := mload(0x40)
// パディングを含む新しい"memory end"です。
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// メモリにコードサイズを格納します。
mstore(code, size)
// 実際のコードを取得します。これはアセンブリが必要です。
extcodecopy(addr, add(code, 0x20), 0, size)
}
}
}
インラインアセンブリは、オプティマイザが効率的なコードを生成できない場合などにも有効です。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library VectorSum {
// この関数は、現在、オプティマイザが配列アクセスにおける境界チェックを除去しないため、効率が悪くなっています。
function sumSolidity(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i)
sum += data[i];
}
// 列へのアクセスは境界内だけであることが分かっているので、チェックを回避できます。
// 最初のスロットに配列の長さが入っているので、0x20を配列に追加する必要があります。
function sumAsm(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i) {
assembly {
sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
}
}
}
// 上記と同じですが、コード全体をインラインアセンブリで実現します。
function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
assembly {
// 長さ(最初の32バイト)を読み込みます。
let len := mload(data)
// 長さのフィールドをスキップします。
//
// in-placeでインクリメントできるように一時的な変数を保持します。
//
// 注: data をインクリメントすると、このアセンブリブロックの後では data 変数は使用できなくなります。
let dataElementLocation := add(data, 0x20)
// 上限に達するまで反復します。
for
{ let end := add(dataElementLocation, mul(len, 0x20)) }
lt(dataElementLocation, end)
{ dataElementLocation := add(dataElementLocation, 0x20) }
{
sum := add(sum, mload(dataElementLocation))
}
}
}
}
外部変数、外部関数、外部ライブラリへのアクセス
Solidityの変数やその他の識別子は、それら名前を使ってアクセスできます。
値型のローカル変数は、インラインアセンブリで直接使用できます。 読み込みと代入の両方が可能です。
メモリを参照するローカル変数は、値そのものではなく、メモリ内の変数のアドレスとして評価されます。 このような変数は代入することもできますが、代入はポインタを変更するだけでデータを変更するわけではないので、Solidityのメモリ管理を尊重する責任があることに注意してください。 Solidityの慣習 を参照してください。
同様に、静的なサイズのcalldata配列やcalldata構造体を参照するローカル変数は、値そのものではなく、calldata内の変数のアドレスに評価されます。
変数に新しいオフセットを割り当てることもできますが、変数が calldatasize() を超えてポイントしないことを保証するための検証は行われないことに注意してください。
外部関数ポインターの場合、アドレスと関数セレクタはそれぞれ x.address と x.selector を使ってアクセスできます。
セレクタは右揃えの4バイトで構成されています。
どちらの値も代入可能です。
例えば、以下のようになります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.10 <0.9.0;
contract C {
// 返り値を格納する変数 @fun に新しいセレクタとアドレスを代入します。
function combineToFunctionPointer(address newAddress, uint newSelector) public pure returns (function() external fun) {
assembly {
fun.selector := newSelector
fun.address := newAddress
}
}
}
動的なcalldata配列の場合、 x.offset と x.length を使ってcalldataのオフセット(バイト単位)と長さ(要素数)にアクセスできます。
両方の式は代入することもできますが、静的の場合と同様に、結果として得られるデータ領域が calldatasize() の範囲内にあるかどうかの検証は行われません。
ローカルストレージ変数や状態変数(transient storage含む)の場合、必ずしも1つのストレージスロットを占有しているわけではないので、単一のYul識別子では不十分です。
そのため、変数の「アドレス」は、スロットとそのスロット内のバイトオフセットで構成されます。
変数 x が指すスロットを取得するには x.slot を、バイトオフセットを取得するには x.offset を使います。
x をそのまま使うとエラーになります。
また、ローカルストレージの変数ポインタの .slot 部に代入することもできます。
これら(構造体、配列、マッピング)の場合、 .offset 部は常にゼロです。
ただし、状態変数の .slot または .offset 部分に代入できません。
ローカルSolidityの変数は代入に利用できます。 例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.28 <0.9.0;
// This will report a warning
contract C {
bool transient a;
uint b;
function f(uint x) public returns (uint r) {
assembly {
// ストレージスロットのオフセットは無視します。
// この特別なケースではゼロであることが分かっています。
r := mul(x, sload(b.slot))
tstore(a.slot, true)
}
}
}
警告
256ビット未満の型( uint64 、 address 、 bytes16 など)の変数にアクセスする場合、その型のエンコーディングに含まれないビットを仮定することはできません。
特に、それらをゼロと仮定してはいけません。
安全のために、このことが重要な文脈で使用する前に、必ずデータを適切にクリアしてください。
uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } 符号付きの型をクリーンにするには、 signextend オペコードを使用できます。
オペコード: assembly { signextend(<num_bytes_of_x_minus_one>, x) }
Solidity 0.6.0以降、インラインアセンブリ変数の名前は、インラインアセンブリブロックのスコープ内で見える宣言(変数宣言、コントラクト宣言、関数宣言を含む)をシャドーイングできません。
Solidity 0.7.0以降、インラインアセンブリブロック内で宣言された変数や関数は . を含むことができませんが、インラインアセンブリブロックの外からSolidityの変数にアクセスするために . を使用することは有効です。
ただし、Solidity を Yul 専用モードで使用している場合は、ドット記法を使っても依然として有効です。
避けるべきこと
インラインアセンブリは、かなりハイレベルな見た目をしていますが、実際には極めてローレベルです。 関数呼び出し、ループ、if、スイッチは簡単な書き換えルールで変換され、その後、アセンブラがしてくれるのは、関数型オペコードの再配置、変数アクセスのためのスタックの高さのカウント、ブロックの終わりに達したときのアセンブリローカル変数のスタックスロットの削除だけです。
Solidityの慣習
型のある変数の値
EVMアセンブリとは対照的に、Solidityには、 uint24 などの256ビットよりも小さい型があります。
効率化のため、ほとんどの算術演算では、型が256ビットよりも短い可能性があるという事実は無視され、高次のビットは必要に応じて、つまり、メモリに書き込まれる直前や比較が実行される前に、クリーニングされます。
つまり、インラインアセンブリ内でこのような変数にアクセスする場合、最初に高次ビットを手動でクリーニングする必要があるかもしれません。
メモリー管理
Solidityは次のような方法でメモリを管理しています。
メモリの位置 0x40 に「フリーメモリポインタ」があります。
メモリを確保したい場合は、このポインタが指す位置から始まるメモリを使用し、更新します。
このメモリが以前に使用されていないという保証はないので、その内容が0バイトであると仮定できません。
割り当てられたメモリを解放するメカニズムは組み込まれていません。
Solidity は、メモリ上の値が任意の値の倍数に揃った位置(アラインされた位置)に配置されることを保証も要求もしません。
以下は、上記のプロセスに沿ってメモリを割り当てるために使用できるアセンブリスニペットです。
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
メモリの最初の64バイトは、短期的に割り当てられる「スクラッチスペース」として使用できます。
フリーメモリポインタの後の32バイト(つまり 0x60 から始まる)は、永久にゼロであることを意味し、空の動的メモリ配列の初期値として使用されます。
つまり、割り当て可能なメモリは、フリーメモリポインタの初期値である 0x80 から始まります。
Solidityのメモリ配列の要素は、常に32バイトの倍数を占めています(これは bytes1[] でも当てはまりますが、 bytes と string では当てはまりません)。
多次元のメモリ配列は、メモリ配列へのポインタです。
動的配列の長さは、配列の最初のスロットに格納され、その後に配列要素が続きます。
警告
静的サイズのメモリ配列にはlengthフィールドがありませんが、静的サイズの配列と動的サイズの配列の間でより良い変換を可能にするために、後に追加されるかもしれませんので、これに頼らないようにしてください。
メモリ安全性
インラインアセンブリを使用しない場合、コンパイラはメモリが常にwell-definedな状態に保たれることに依存できます。 これは特に Yul IRによる新しいコード生成パイプライン に関連しています。 このコード生成パスは、メモリの使用に関する特定の仮定に依存できる場合、スタックからメモリにローカル変数を移動してStack Too Deepを回避し、追加のメモリの最適化を実行できます。
Solidityのメモリモデルを常に尊重することをお勧めしますが、インラインアセンブリでは互換性のない方法でメモリを使用できます。 したがって、スタック変数をメモリに移動する処理やその他のメモリ最適化は、メモリ操作またはメモリにSolidity変数を割り当てる操作を含むインラインアセンブリブロックの存在下でデフォルトでグローバルに無効になっています。
ただし、次のようにアセンブリブロックに特別な注釈を付けて、Solidityのメモリモデルを尊重していることを示すことができます:
assembly ("memory-safe") {
...
}
特に、メモリセーフなアセンブリブロックは、以下のメモリ範囲にのみアクセスできます:
上記の
allocate関数のようなメカニズムを使用して自分で割り当てたメモリ。Solidityによって割り当てられたメモリ(例: 参照するメモリ配列の境界内のメモリ)。
先述したメモリオフセット0と64の間のスクラッチスペース。
アセンブリブロックの開始時点のフリーメモリポインタの値より 後 に位置する一時的なメモリ。 すなわち、フリーメモリポインタを更新することなく、フリーメモリポインタに「割り当て」られたメモリ。
さらに、アセンブリブロックがメモリ上にSolidity変数を割り当てる場合、Solidity変数へのアクセスがこれらのメモリ範囲にのみアクセスすることを保証する必要があります。
これは主にオプティマイザに関するものなので、アセンブリブロックがリバートしたり終了したりしても、これらの制限に従う必要があります。
例として、次のアセンブリスニペットはメモリセーフではありません。
なぜなら returndatasize() の値はスクラッチスペースの範囲である64バイトを超える可能性があるからです:
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
一方、次のコード は メモリセーフです。 なぜなら、フリーメモリポインタが指す位置より先のメモリは、一時的なスクラッチスペースとして安全に使用できるからです。
assembly ("memory-safe") {
let p := mload(0x40)
returndatacopy(p, 0, returndatasize())
revert(p, returndatasize())
}
次の割り当てがない場合は、フリーメモリポインタを更新する必要はありませんが、フリーメモリポインタが与える現在のオフセットから始まるメモリのみを使用できることに注意してください。
メモリ操作で長さ0を使用する場合は、任意のオフセットを使用しても問題ありません(スクラッチスペースに該当する場合のみではありません):
assembly ("memory-safe") {
revert(0, 0)
}
インラインアセンブリ自体のメモリ操作だけでなく、メモリ上の参照型のSolidity変数への代入もメモリセーフにならないことがあることに注意してください。 例えば以下のようなものはメモリセーフではありません:
bytes memory x;
assembly {
x := 0x40
}
x[0x20] = 0x42;
メモリにアクセスする操作や、メモリ上のSolidity変数への代入を行わないインラインアセンブリは、自動的にメモリセーフとみなされ、アノテーションを付ける必要はありません。
警告
アセンブリが実際にメモリモデルを満たしているかどうかを確認するのは、あなたの責任です。 アセンブリブロックをメモリセーフとアノテーションしても、メモリの前提条件の1つに違反した場合、テストでは容易に発見できない不正確で未定義の動作につながるでしょう。
Solidityの複数のバージョンで互換性のあるライブラリを開発する場合、特別なコメントを使用してアセンブリブロックをメモリセーフとして注釈できます:
/// @solidity memory-safe-assembly
assembly {
...
}
なお、コメントによるアノテーションは、将来のブレーキングリリースで禁止する予定です。 したがって、古いコンパイラのバージョンとの後方互換性にこだわらない場合は、方言文字列を使用することをお勧めします。
Advanced Safe Use of Memory
Beyond the strict definition of memory-safety given above, there are cases in which you may want to use more than 64 bytes
of scratch space starting at memory offset 0. If you are careful, it can be admissible to use memory up to (and not
including) offset 0x80 and still safely declare the assembly block as memory-safe.
This is admissible under either of the following conditions:
By the end of the assembly block, the free memory pointer at offset
0x40is restored to a sane value (i.e. it is either restored to its original value or an increment of it due to a manual memory allocation), and the memory word at offset0x60is restored to a value of zero.The assembly block terminates, i.e. execution can never return to high-level Solidity code. This is the case, for example, if your assembly block unconditionally ends in calling the
revertopcode.
Furthermore, you need to be aware that the default-value of dynamic arrays in Solidity point to memory offset 0x60, so
for the duration of temporarily changing the value at memory offset 0x60, you can no longer rely on getting accurate
length values when reading dynamic arrays, until you restore the zero value at 0x60. To be more precise, we only guarantee
safety when overwriting the zero pointer, if the remainder of the assembly snippet does not interact with the memory of
high-level Solidity objects (including by reading from offsets previously stored in variables).