障害解析のためのアセンブリ言語
クラッシュしたアプリケーションを調査するとき、ソースコードやローカル変数を十分に参照できるとは限りません。逆アセンブルを読むと、CPUがどのアドレスを読み書きしようとしたか、どの条件で分岐したか、どの関数を呼び出そうとしたかを確認できます。
このシリーズでは、アセンブリでプログラムを書くことよりも、WindowsのネイティブアプリケーションをWinDbgなどで調査するための読み方に重点を置きます。対象はx86、x64、ARM64(AArch64)です。ARM64ECやマネージドコードのJIT固有の呼び出し規約は対象外とします。
読む順序
| 記事 | 調査で分かること |
|---|---|
| 汎用レジスター | 停止位置、ポインター、演算結果、条件フラグをどこで確認するか |
| データ操作命令 | メモリーを何バイト読み書きするか、アクセス先をどう計算するか |
| 算術演算とビット操作 | 加減算、符号拡張、桁あふれ、シフトをどう解釈するか |
| 制御フロー命令 | 比較結果からどちらへ分岐するか、ループをどう読むか |
| スタック操作と関数呼び出し | 戻りアドレス、退避レジスター、スタック上のローカル変数をどう探すか |
| 呼び出し規約 | 実際の入口・関数途中のダンプから、引数をどこまで復元できるか |
| C/C++のコードと命令の対応 | 配列、構造体、仮想関数などを逆アセンブルでどう見分けるか |
本シリーズの表記
- x86・x64はWinDbgやVisual Studioで使われるIntel構文で記載します。通常、左側が書き込み先です。
- ARM64も原則として左側が書き込み先ですが、
strなどは先頭のレジスターの値をメモリーへ書き込みます。 0x20と20hはどちらも16進数の32を表します。ARM64の#は即値を示し、#32は10進数の32です。- プロンプト付きの出力は、共通のC++サンプルをビルドし、CDBで取得した実測例です。抜粋では省略範囲を明記しています。命令形式の比較表や、明示した計算例は説明用です。
- コードブロックは、x86・x64に
x86asm、ARMにarmasmを使用します。当サイトではx86asmをPrismのnasm定義の別名として登録しています。これは表示上のハイライト指定であり、WinDbgの構文や対象CPUを変更するものではありません。 - Windowsでは、x86のポインターは4バイト、x64・ARM64のポインターは8バイトです。
intとlongはこれらの環境でいずれも4バイトです。
CPUが定める命令の動作と、WindowsのABIが定める引数の渡し方は別のものです。また、同じC/C++ソースでも、最適化設定やコンパイラーによって命令列は変わります。
例外発生位置を確認する
例外情報を含むユーザーモードダンプでは、まず例外コンテキストを選んでからレジスターと命令を確認します。
.exr -1
.ecxr
r
u
k
.exr -1は例外レコード、.ecxrは例外発生時のレジスターコンテキスト、rはレジスター、uは逆アセンブル、kはコールスタックを表示します。例外コンテキストを持たないハングダンプでは、対象スレッドを選んでその停止位置を調べます。
本シリーズで取得したx86の実例では、frame_probeの__debugbreak()で保存したダンプを再度開き、.ecxrの後に次の状態を確認できました。
0:000> r eip,esp,ebp,eax,ecx,edx
Last set context:
eip=003f1163 esp=02effd28 ebp=02effd34 eax=00000042 ecx=0000008f edx=0000000b
この例は意図的なブレークポイント停止であり、アクセス違反ではありません。命令ポインターはint 3を指し、レジスターには引数そのものではなく計算途中の値も残っています。ソース、命令、ダンプの対応は呼び出し規約で説明します。
アクセス違反なら、例外レコードのアクセス種別とアドレスに加え、障害命令の転送幅と実効アドレスを照合します。データ操作命令とバッファ境界の確認を参照してください。
実測例を自分の環境で確認する
共通のsample.cppをダウンロードできます。記事ごとに別の処理を手書きアセンブリで作るのではなく、このソースから生成された命令を比較しています。各例はMSVC 19.51.36256、CDB 10.0.28000.2526で取得しました。
Visual Studioの対象アーキテクチャのC++ビルド環境で、次のようにコンパイルします。実行ファイルとPDBが混在しないよう、アーキテクチャごとに作業ディレクトリを分けます。
cl /nologo /W4 /WX /O2 /Ob0 /MT /Zi /Fesample.exe /Fosample.obj /Fdsample.pdb sample.cpp /link /DEBUG /INCREMENTAL:NO /OPT:NOREF /OPT:NOICF
インライン化は無効ですが、その他の最適化は有効です。x86のフレームを説明する今回の追加実測では、さらに/Oy-を付け、フレームポインターを保持しています。 過去の取得例とは配置やアドレスが異なります。/OPT:NOREFは未参照関数の除去、/OPT:NOICFは同一関数の統合を抑止する指定です。これらは比較用の条件で、通常のアプリケーションに必須のビルド設定ではありません。
CDBを実行ファイルとPDBのディレクトリから起動すると、最初の停止位置で関数を表示できます。次は入力例です。
cdb.exe -y . sample.exe
uf sample!read_element
uf sample!scaled_bytes
uf sample!positive_or_zero
uf sample!call_pair
q
取得環境はWindows on ARMで、x86はx86版CDB、x64・ARM64はARM64版CDBを使用しました。x64版サンプルの命令を読む際は.effmach amd64でモードを指定しています。ネイティブ側のARM64コードも含まれるため、プロセス内のすべての命令を同じモードで読めるわけではありません。
引数付きでsample.exe frameをCDBから起動すると、引数とローカル変数を調べるframe_probeへ進みます。途中に__debugbreak()を含むデバッグ用の実行モードです。入口と途中でダンプを保存して再度開く手順、および今回のx64エミュレーションダンプでのコンテキスト選択上の制約は、呼び出し規約を参照してください。
読み違いを避けるために
- 調査対象の命令セットを確認します。64ビットWindows上でも、WOW64プロセスはx86コードです。ARM64版Windowsでも、対象がネイティブARM64とは限りません。
- レジスターは、選択したスレッドとコンテキストに属します。別のスレッドや呼び出し元フレームの値と混同しないことが重要です。
- 停止中の命令が実行される前なのか、例外を起こした命令なのかを区別します。例外の種類によっては、命令の一部の処理が済んでいることもあります。
- 通常のダンプは実行履歴ではありません。前の命令が表示できても、その経路を実際に通ったことまでは証明できません。
- シンボルが不一致の場合や必要なメモリーがダンプに含まれない場合は、変数やポインターを十分に復元できません。読み取り不能を、そのまま実行時のメモリー破損と判断しないようにします。
参考資料
- Intel 64 and IA-32 Architectures Software Developer's Manuals — x86・x64の命令仕様
- Arm A64 Instruction Set Architecture — ARM64の命令仕様
- x64 architecture | Microsoft Learn
- Overview of ARM64 ABI conventions | Microsoft Learn
- .ecxr (Display Exception Context Record) | Microsoft Learn
- u, ub, uu (Unassemble) | Microsoft Learn
📄️ 汎用レジスター
Windowsのx86・x64・ARM64で、命令ポインター、スタック、引数、戻り値、条件フラグと、部分レジスターへの書き込みの読み方を解説します。
📄️ データ操作命令
MOV・LEAとARM64のLDR・STRから、読み書きの方向、アクセス幅、実効アドレスを求め、アクセス違反を調べる方法を解説します。
📄️ 算術演算とビット操作
x86・x64・ARM64の加減算、乗除算、ビット操作と条件フラグを、整数の桁あふれやサイズ計算の障害解析に結び付けて解説します。
📄️ 制御フロー命令
x86・x64・ARM64の比較、条件分岐、ループ、間接分岐を読み、符号付きと符号なしの違いやダンプ解析の限界を確認します。
📄️ スタック操作と関数呼び出し命令
Windowsのx86・x64・ARM64で、スタック、戻りアドレス、プロローグとエピローグ、アンワインドを障害解析の視点から解説します。
📄️ 呼び出し規約
Windowsのx86呼び出し規約、x64のシャドウスペース、ARM64の引数と戻り値を比較し、ダンプから引数を読む際の注意点を解説します。
📄️ C/C++のコードとアセンブリ命令の対応
WinDbgの実測出力から配列コピー、文字列操作、仮想関数とCOMのテーブルを読み、レジスターとバッファ境界を照合するデバッグの勘所を解説します。