x86は、大規模で不規則な命令セットを、データ移動、算術演算、ビット操作、制御フロー、文字列処理という共通のパターンに分類している。オペランドサイズ、アドレスサイズ、実行モード、プレフィックス、ISA拡張によって、命令のエンコーディングや利用可能性が変わる場合がある。IntelとAMDのアーキテクチャマニュアルが引き続き権威ある資料であり、アセンブルに成功したことは、ツールがソースをエンコードできることを示すだけである。
データ移動とアドレス生成 #
MOVは、即値またはレジスタ値をレジスタまたはメモリへコピーし、あるいはメモリからレジスタへコピーする。通常のMOVが持てる明示的なメモリオペランドは最大1つである。MOVZXはより狭い整数をゼロ拡張し、MOVSXとMOVSXDは符号拡張する。64ビットモードで32ビット汎用レジスタを宛先にすると、上位32ビットが暗黙的にクリアされる。
CMOVccは、制御フローを変更せず、ステータスフラグに従ってレジスタまたはメモリのソースを条件付きでコピーする。メモリソースの条件付き移動では、結果を選択する前にソースの読み出しが行われる場合があるため、無効なアドレスへのアクセスを防ぐ目的で使用してはならない。SETccは、同じ条件ファミリに基づいてバイトの結果を書き込む。
XCHGは2つのオペランドを交換する。メモリ形式は明示的なLOCKプレフィックスがなくてもロックされたセマンティクスを持つため、指定されたメモリに対して他のプロセッサから見てアトミックに実行される。アトミック性が複合アルゴリズムの正しさを保証するわけではない。順序付け、キャッシュラインの配置、再試行動作も依然として重要である。
LEAはメモリにアクセスせずに実効アドレスを計算し、その算術結果を書き込む。
lea rax, [rdi + rdi*4] ; rax = rdi * 5コンパイラは、ポインタ計算や、特定の加算または定数乗算にLEAを使用する。メモリオペランドを逆参照しないため、セグメントベースやページングは適用されない。
スタック操作 #
PUSHはスタックポインタを減算して値を保存し、POPは値を読み出してスタックポインタを加算する。正確なオペランドサイズの動作はモードに依存する。アライメント、レッドゾーン、シャドースペース、保存レジスタ、そしてスタック保存領域として有効なメモリを決めるのは命令ではなく、プラットフォームABIである。
CALLは復帰アドレスを保存して制御を移し、near形式のRETはスタックから復帰アドレスを読み出す。したがって、スタックポインタの不一致や破損した復帰アドレスは制御フローを変更する。far call、far return、コールゲート、IRETはさらにセグメント状態または特権状態を処理するため、通常の関数呼び出し命令として扱ってはならない。
整数算術、比較、フラグ #
ADD、ADC、SUB、SBBは、通常精度および多倍長の算術演算を実装する。ADCはキャリーフラグCFを消費し、SBBはそれをボローとして消費するため、下位から上位へのリム列はキャリーまたはボローを伝播する。INCとDECは複数の算術フラグを更新するが、CFは保持する。
CMPはフラグを設定するためだけに減算を実行する。TESTはフラグを設定するためだけにビット単位のANDを実行する。その後、条件分岐、条件付き移動、バイト設定命令がそれらのフラグを解釈する。
- 等値比較には
ZFを使用する。 - 符号なしの大小比較には
CFとZFを使用する。 - 符号付きの大小比較には
SF、OF、ZFを組み合わせる。
CFは符号なしのキャリーまたはボローを記録し、OFは符号付きオーバーフローを記録する。したがって、同じビットパターンでも、プログラムにおけるデータの解釈に一致する符号付きまたは符号なし条件が必要になる。
単一オペランド形式のMULとIMULは、アキュムレータに明示的なオペランドを乗算し、暗黙の上位および下位レジスタに倍幅の結果を生成する。2オペランド形式および3オペランド形式のIMULは、切り詰めた結果を明示的な宛先に書き込む。DIVとIDIVは、暗黙の倍幅被除数を除算する。除数がゼロの場合、または商が宛先に収まらない場合は除算エラーを発生させる。ソフトウェアは、除算前に上位半分を正しく準備しなければならない。
論理、ビットテスト、シフト、ローテート操作 #
AND、OR、XOR、NOTはビット単位で動作する。xor eax, eaxによってレジスタをクリアする方法は、現在のアウトオブオーダープロセッサで依存関係を断ち切る一般的なイディオムだが、アーキテクチャ上の結果は単にゼロである。
BTは選択したビットをCFへコピーし、BTS、BTR、BTCはそのビットをそれぞれセット、リセット、反転する。メモリビットインデックスは、エンコードされたメモリオペランドの範囲を超えてアドレス指定できるため、境界の検討が重要になる。明示的なLOCKプレフィックスにより、サポートされているメモリ形式のリード・モディファイ・ライトをアトミックにできる。
BSFとBSRは、最下位または最上位のセットビットを検索する。ソースがゼロの場合、宛先は未定義である。関連する拡張が存在する場合、TZCNTとLZCNTはゼロ入力に対して定義された結果を提供するが、その拡張を持たないプロセッサでは、それらのエンコーディングがBSFまたはBSRとして実行される可能性がある。したがって、機能検出が必要である。
SHLとSALは同等の左シフトである。SHRはゼロビットを挿入し、SARは符号ビットを複製する。ROLとRORはオペランド内でローテートし、RCLとRCRはCFを介してローテートする。シフトカウントはオペランドサイズに従ってマスクされ、大きなカウントまたはゼロカウントに対するフラグ結果については命令定義を参照する必要がある。
直接および間接制御フロー #
near形式のJMP、CALL、条件付きJcc命令は、命令末尾を基準に測定した相対変位を使用できる。間接JMPとCALLは、レジスタまたはメモリから宛先を読み出す。RETはスタックから宛先を取得する。間接ターゲットは制御フローインテグリティ、リターン予測、投機実行の緩和策と相互作用するため、同等に見える形式でもセキュリティまたは性能特性が異なる場合がある。
短い条件分岐は、8ビットの符号付き変位をエンコードする。near形式は、32ビットモードおよび64ビットモードでより広い変位を使用する。x86には、条件付きでcallまたはreturnを実行する汎用命令はない。コードでは条件分岐を組み合わせてこれらの操作を構成する。
例外、ソフトウェア割り込み、外部割り込みは、割り込みディスクリプタテーブルを経由して転送される。INT、IRET、ゲートベースの転送には、通常の分岐を超える特権セマンティクスがある。SYSCALL/SYSRETおよびSYSENTER/SYSEXITは、モデル固有の設定とオペレーティングシステムABIを使用する。
文字列命令とリピートプレフィックス #
MOVS、CMPS、SCAS、LODS、STOSは、暗黙のソース、宛先、アキュムレータ、カウントレジスタを使用する。バイト、ワード、ダブルワード、および一部のクワッドワード形式では、要素サイズが明示される。方向フラグ(DF)は、インデックスレジスタをインクリメントするかデクリメントするかを選択する。
REPは、カウントがゼロでない間、操作を繰り返す。REPE/REPZとREPNE/REPNZは、比較またはスキャン命令でZFも使用する。現在のプロセッサは大規模なREP MOVSBおよびREP STOSB操作を最適化できるが、機能とサイズに応じたライブラリディスパッチは実装上の判断である。
宛先がソース範囲の内部かつ上位から始まる場合、前方のrep movsbは安全ではない。初期のストアが、まだ読み取られていないバイトを上書きするためである。後方コピーによって、その重複を処理できる。
std ; インデックスをデクリメント
rep movsb
cld ; ABIで要求される方向を復元呼び出し側は、後方コピーのためにポインタを最後の要素へ初期化しなければならない。ほとんどのx86 ABIは、関数の入口と復帰時にDF=0を要求するため、STDを実行するコードは失敗経路でも復元しなければならない。実運用のmemmove実装も、重複、サイズ、プロセッサ機能に応じて方向と最適化されたシーケンスを選択する。
CLI、STI、LGDT、LIDT、制御レジスタアクセス、ポートI/O、HLTなどの特権操作は、現在のモードと特権によって制限される。アセンブラが受け付けたことは、実行権限を与えるものではない。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)