汎用レジスター
レジスターは、CPUが演算対象やアドレスなどを保持するための領域です。ダンプを読む際は、まず「どこで停止したか」「何を参照しているか」「どこから呼ばれたか」を追うためのレジスターを確認すると取り掛かりやすくなります。
本記事はWindowsのネイティブコードを対象とします。以下で示す引数や戻り値の役割は、CPU固有の制約ではなく、主にWindowsの呼び出し規約による約束です。汎用レジスターの用途は関数の途中で変わります。
最初に見るレジスター
| 目的 | x86 | x64 | ARM64 |
|---|---|---|---|
| 命令の位置 | eip | rip | pc |
| スタックの先頭 | esp | rsp | sp |
| 一般的な整数・ポインターの戻り値 | eax | rax | x0 / w0 |
| 条件フラグ | eflags | rflags(表示ではeflなど) | NZCV(デバッガーではcpsr内などに表示) |
通常のブレークポイントなどで停止した場合、命令ポインターは再開後に実行する命令を示します。例外時は例外の種類によって解釈が異なるため、例外レコードとコンテキストを照合します。
x86
x86の基本となる汎用レジスターは32ビットです。
| 名称 | 典型的な使い方 |
|---|---|
eax | 32ビット整数やポインターの戻り値、演算結果 |
ebx | 呼び出しをまたいで保持したい値など |
ecx | __thiscallのthis、__fastcallの引数、繰り返し命令の回数 |
edx | __fastcallの引数、edx:eaxを使う乗除算の上位部分など |
esi | 文字列・メモリー転送命令の読み取り元など |
edi | 文字列・メモリー転送命令の書き込み先など |
ebp | フレームポインター、または最適化により別用途 |
esp | スタックポインター |
たとえばrep movsbでは、通常、esiが読み取り元、ediが書き込み先、ecxが転送回数です。一方、通常の関数内でesiにオブジェクトのポインターを保持していても不思議ではありません。名称だけで役割を固定せず、値を設定した命令から追います。
x64
x64は、x86のレジスターを64ビットへ拡張し、r8~r15を追加したものです。
| 名称 | Windows x64での典型的な役割 |
|---|---|
rax | 整数・ポインターの戻り値、演算結果 |
rcx、rdx、r8、r9 | 先頭4引数の位置に対応する整数・ポインター |
r10、r11 | 一時値 |
rbx、rbp、rsi、rdi、r12~r15 | 呼び出し先が保存・復元する非揮発レジスター |
rsp | スタックポインター |
rip | 命令ポインター |
rcxなどは関数の入口では引数を保持しますが、後続の命令や別関数の呼び出しで上書きされます。クラッシュ時のrcxを、そのまま元の第1引数と考えることはできません。
また、rbpを常にフレームポインターとして使うわけではありません。x64のスタック復元ではアンワインド情報も利用します。
部分レジスター
| 64ビット | 下位32ビット | 下位16ビット | 下位8ビット |
|---|---|---|---|
rax | eax | ax | al |
rcx | ecx | cx | cl |
rsp | esp | sp | spl |
r8 | r8d | r8w | r8b |
これらは別々の保存領域ではなく、同じレジスターの一部です。特に、x64で32ビットレジスターへ書き込む命令は、対応する64ビットレジスターの上位32ビットを0にします。
次のC++関数をMSVCの/O2 /Ob0でビルドし、CDBで確認します。共通サンプルと取得条件はシリーズの入口にまとめています。
unsigned long long zero_extend(unsigned int value) { return value; }
0:000:AMD64> uf sample!zero_extend
sample!zero_extend:
00007ff6`81521410 8bc1 mov eax,ecx
00007ff6`81521412 c3 ret
戻り値は64ビットですが、生成命令は32ビットのmov eax,ecxです。これによりraxの上位32ビットも0になるため、符号なし整数の64ビットへの変換になります。一方、alやaxへ書き込んだ場合、残りのビットは保持されます。ポインターの上位ビットが失われる障害では、直前の命令の書き込み幅を調べることが重要です。
ARM64
ARM64の汎用レジスターはx0~x30です。下位32ビットを使う場合はw0~w30と表記します。Windowsの標準的なARM64 ABIでは次の役割があります。
| 名称 | 役割 |
|---|---|
x0~x7 | 主な整数・ポインター引数。単純な整数・ポインターの戻り値はx0 |
x8 | 一部の構造体戻り値の出力先など。常に第9引数という意味ではない |
x9~x15 | 一時値 |
x16、x17 | 呼び出し時の中継コードなどでも使われる一時レジスター |
x18 | Windowsのプラットフォーム予約レジスター。ユーザーモードではTEBを指す |
x19~x28 | 非揮発レジスター |
x29 / fp | フレームポインター |
x30 / lr | リンクレジスター。bl・blrで戻り先を設定 |
sp | スタックポインター |
pc | 命令の位置。x0などと同じようには操作できない |
xzr / wzr | 読み出すと0になり、書き込みを捨てるゼロレジスター |
spとゼロレジスターは同じものではありません。命令中のレジスター番号31が、命令の形式によってどちらかを表します。
w0へ書き込むとx0の上位32ビットは0になります。これはx64のeaxへの書き込みと似ています。w0に0xffffffffを設定しても、x0全体が64ビットの-1になるわけではありません。
同じzero_extendのARM64ビルドでは、CDBは次のように表示しました。
0:000> uf sample!zero_extend
sample!zero_extend:
00007ff6`7bef4490 d3407c00 uxtw x0,w0
00007ff6`7bef4494 d65f03c0 ret
uxtwは32ビット値のゼロ拡張です。符号拡張が必要な場合との比較はデータ操作命令を参照してください。
x30は直近の呼び出し命令で上書きされます。関数がさらに別の関数を呼ぶ場合、元の戻りアドレスはスタックなどに退避する必要があります。詳細はスタック操作と関数呼び出しで扱います。
条件フラグ
| 意味 | x86・x64 | ARM64 |
|---|---|---|
| 結果が0 | ZF | Z |
| 結果の最上位ビットが1 | SF | N |
| 符号なしのキャリー(桁上がり)など | CF | C |
| 符号付き演算の桁あふれ | OF | V |
減算では、x86・x64のCFはボロー(桁借り)が発生した場合に1、ARM64のCはボローが発生しなかった場合に1となります。条件フラグは独立した変数ではなく、命令ごとの規則で更新されます。ARM64のaddとaddsの違いなどは算術演算とビット操作を参照してください。
浮動小数点・SIMDレジスター
汎用レジスターだけでは、浮動小数点引数やベクトル演算を説明できません。
- x86ではx87の
st(0)などや、SSEのxmm0などが使われます。 - Windows x64では浮動小数点の引数・戻り値にXMMレジスターを使います。
- ARM64では
v0~v31があり、q0・d0・s0などはそのサイズ別の見方です。通常の浮動小数点引数は主にv0~v7の対応する部分を使います。
整数やポインターを対象にした引数表だけで、浮動小数点や構造体の引数を判断しないようにします。
WinDbgで確認する
次は、引数を調べるサンプルのx86ダンプを開き、.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
ここではeipがframe_probe内のint 3、espが現在のスタック先頭です。eaxの0x42は補助関数の戻り値66、ecxの0x8fは計算途中の143で、どちらも元の引数ではありません。値の意味は、レジスター一覧だけではなく、そこへ書き込んだ命令と照合して解釈します。
rで現在のコンテキストのレジスターを表示します。x64で個別に見る場合は、たとえばr rcxやr ripです。例外情報を含むユーザーモードダンプでは.ecxrで例外コンテキストを選んでから確認します。
ダンプには全レジスターの履歴はありません。また、呼び出し元フレームを選んでも、その時点の揮発レジスターまで復元できるとは限りません。値が分からない場合は、呼び出し元の命令や保存先メモリーから確認できる範囲を明確にするとよいでしょう。