メインコンテンツまでスキップ

汎用レジスター

レジスターは、CPUが演算対象やアドレスなどを保持するための領域です。ダンプを読む際は、まず「どこで停止したか」「何を参照しているか」「どこから呼ばれたか」を追うためのレジスターを確認すると取り掛かりやすくなります。

本記事はWindowsのネイティブコードを対象とします。以下で示す引数や戻り値の役割は、CPU固有の制約ではなく、主にWindowsの呼び出し規約による約束です。汎用レジスターの用途は関数の途中で変わります。

最初に見るレジスター​

目的x86x64ARM64
命令の位置eiprippc
スタックの先頭esprspsp
一般的な整数・ポインターの戻り値eaxraxx0 / w0
条件フラグeflagsrflags(表示ではeflなど)NZCV(デバッガーではcpsr内などに表示)

通常のブレークポイントなどで停止した場合、命令ポインターは再開後に実行する命令を示します。例外時は例外の種類によって解釈が異なるため、例外レコードとコンテキストを照合します。

x86​

x86の基本となる汎用レジスターは32ビットです。

名称典型的な使い方
eax32ビット整数やポインターの戻り値、演算結果
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ビット
raxeaxaxal
rcxecxcxcl
rspespspspl
r8r8dr8wr8b

これらは別々の保存領域ではなく、同じレジスターの一部です。特に、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呼び出し時の中継コードなどでも使われる一時レジスター
x18Windowsのプラットフォーム予約レジスター。ユーザーモードでは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・x64ARM64
結果が0ZFZ
結果の最上位ビットが1SFN
符号なしのキャリー(桁上がり)などCFC
符号付き演算の桁あふれOFV

減算では、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で例外コンテキストを選んでから確認します。

ダンプには全レジスターの履歴はありません。また、呼び出し元フレームを選んでも、その時点の揮発レジスターまで復元できるとは限りません。値が分からない場合は、呼び出し元の命令や保存先メモリーから確認できる範囲を明確にするとよいでしょう。

参考資料​