All Articles

Magical WinDbg VOL.1【7 章 フルメモリダンプからユーザモードメモリリーク事象を調査する】

本書の最後の章である 7 章では、6 章で解析したものと同じユーザモードのメモリリーク事象をシステムのフルメモリダンプから解析していきます。

メモリリーク事象の調査方法自体は 6 章と同じですので、7 章はどちらかというとフルメモリダンプから様々な情報を抜き出すテクニックの紹介が中心となります。

しかし、カーネルモードのメモリ情報を含むフルメモリダンプを解析する場合には、ユーザモードのプロセスダンプ解析と比較して使用可能なコマンドや出力結果が異なるため、また違ったアプローチでの解析を楽しむことができると思います。

もくじ

システムのフルメモリダンプの取得

解析用のフルメモリダンプを取得するため、6 章の手順と同じく、D4C.exe を起動し、2 番のメニューを選択してアプリケーションのメモリリーク事象を再現します。

D4C.exe でユーザモードメモリリーク事象の再現

Process Explorer などのツールを使用して D4C.exe のプロセスが使用する仮想メモリ領域が肥大化したことを確認したら、キーボード操作によってシステムクラッシュを発生させることでフルメモリダンプを取得します。

キーボード操作によるシステムクラッシュは、1 章と同じ手順でキーボードの右 Ctrl キーを押しながら、Space キーを 2 回連打することで引き起こすことが可能です。

1 章で実施したキーボードクラッシュの設定が反映されている場合、上記のキー操作を実施するとシステムがクラッシュし、ブルースクリーン画面が表示されます。

ブルースクリーン(BSOD)

システムの再起動後、C:\Windows フォルダ直下に仮想マシンの物理メモリとほぼ同サイズの FULL_MEMORY.DMP が生成されます。

本章では、このフルメモリダンプを使用してアプリケーションのメモリリーク事象の調査を行います。

ちなみに、1 章でも記載した通り、本書の手順で設定したキーボードクラッシュは、 RDP 経由で接続している場合には有効に動作しません。

物理キーボードを使用可能な場合は、ローカルマシンに直接サインインした状態で「右 Ctrl キーを押しながら、Space キーを 2 回連打」することでシステムクラッシュを発生させる必要があります。

また、Hyper-V 仮想マシンを利用している場合には、拡張セッションを無効化した状態でマシンにサインインし、「右 Ctrl キーを押しながら、Space キーを 2 回連打」することでキーボードクラッシュを行うことが可能です。

その他の仮想マシンを利用している場合には、ソフトウェアキーボードの使用を試してみてください。

もしキーボードクラッシュを実施できない環境の場合は、[Sysinternals ユーティリティのインストール] でダウンロードした SysinternalsSuite に含まれる notmyfault.exe を使用することでも、意図的にシステムクラッシュを再現することが可能です。

WinDbg にフルメモリダンプをロードする

解析用のダンプファイルを取得できたら、さっそく管理者権限で起動した WinDbg にロードしましょう。

これまでの章と同じく、!analyze -v コマンドを実行してダンプファイルに含まれる Bugcheck の情報を参照すると、以下のように MANUALLY_INITIATED_CRASH (e2)1 と表示されました。

0: kd> !analyze -v

MANUALLY_INITIATED_CRASH (e2)
The user manually initiated this crash dump.
Arguments:
Arg1: 0000000000000000
Arg2: 0000000000000000
Arg3: 0000000000000000
Arg4: 0000000000000000

これは、ユーザがカーネルデバッガやキーボード操作によって意図的にシステムクラッシュを引き起こした場合に記録される値です。

つまり、6 章で手動生成したプロセスダンプと同じく、フルメモリダンプに含まれる例外発生時のコンテキストがメモリリーク事象の解析には役に立たないものであることがわかります。

そのため、フルメモリダンプからクラッシュ以外の問題の調査を行う場合は、まず適切な解析対象を絞り込み、デバッガのコンテキストを合わせる必要があります。

そこで、ここからの項では、適切な解析対象を特定するために、フルメモリダンプからシステム内の情報を網羅的に収集していきます。

フルメモリダンプにはシステムの物理メモリ上に保持されているすべてのページの情報が含まれており、高機能なデバッガである WinDbg の機能をフルに使用することで、システム内のあらゆる情報をフルメモリダンプから取得できます。

実行するコマンドによっては出力結果が膨大になる場合があるので、必要に応じて .logopen コマンドで出力結果をファイルに書き出すようにしておくと良いでしょう。

端末のハードウェア情報を収集する

はじめに、!sysinfo 拡張機能2を使用してダンプファイルに記録されているハードウェアの情報を収集してみます。

!sysinfo 拡張機能にはいくつかのオプションがありますが、本書では CPU の情報を表示する !sysinfo cpuinfo コマンドと端末情報を表示する !sysinfo machineid コマンドを使用してみました。

# CPU の情報を表示
0: kd> !sysinfo cpuinfo
[CPU Information]
~MHz = REG_DWORD 1992
Component Information = REG_BINARY 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
Configuration Data = REG_FULL_RESOURCE_DESCRIPTOR ff,ff,ff,ff,ff,ff,ff,ff,0,0,0,0,0,0,0,0
Identifier = REG_SZ Intel64 Family 6 Model 142 Stepping 10
ProcessorNameString = REG_SZ Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
Update Status = REG_DWORD 7
VendorIdentifier = REG_SZ GenuineIntel
MSR8B = REG_QWORD ea00000000

# 端末情報を表示する
0: kd> !sysinfo machineid
Machine ID Information [From Smbios 3.0, DMIVersion 0, Size=3046]
BiosMajorRelease = 1
BiosMinorRelease = 43
FirmwareMajorRelease = 1
FirmwareMinorRelease = 10
BiosVendor = LENOVO
BiosVersion = N20ET58W (1.43 )
BiosReleaseDate = 07/26/2021
SystemManufacturer = LENOVO
SystemProductName = 20KES0KB00
SystemFamily = ThinkPad X280
SystemVersion = ThinkPad X280
SystemSKU = LENOVO_MT_20KE_BU_Think_FM_ThinkPad X280
BaseBoardManufacturer = LENOVO
BaseBoardProduct = 20KES0KB00
BaseBoardVersion = Not Defined

このコマンドを実行することで、上記のようにシステムのクラッシュが発生した端末が Intel i7-8550U を搭載した ThinkPad X280 であることを確認できました。

CPU についてさらに詳細な情報を表示したい場合、!cpuinfo 拡張機能3を使用できます。

!cpuinfo コマンドをオプション無しで実行すると、すべてのプロセッサの情報が表示されます。

!cpuinfo 拡張機能の実行結果

Intel i7-8550U は 4 コア 8 スレッドの CPU ですので、!cpuinfo コマンドの実行結果も 8 行になっています。

CP 列の 0 から 7 までの値は各プロセッサを表しており、MHz 列は周波数を表しています。

システム情報を収集する

次に、フルメモリダンプからダンプファイルを取得した OS のシステム情報を収集します。

OS のシステム情報については、ユーザが実行しているプロセスにコンテキストを合わせた状態で !peb コマンドを実行することで、PEB に含まれるユーザの環境変数の情報から参照することが可能です。

実際に、今回の解析対象のフルメモリダンプからも、以下のようにコンピュータ名や CPU の数、PATH 環境変数やユーザ名などの情報を取得することができました。

0: kd> !peb
{{ 省略 }}
Environment:  000001d5a58027f0
  ...
  COMPUTERNAME=THINKPAD-X280
  ...
  NUMBER_OF_PROCESSORS=8
  OS=Windows_NT
  Path=C:\Program Files\Common Files\Oracle\Java\javapath;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\Intel\WiFi\bin\;C:\Program Files\Common Files\Intel\WirelessCommon\;C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Local\Microsoft\WindowsApps
  ...
  PROCESSOR_ARCHITECTURE=AMD64
  PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
  ...
  USERDOMAIN=THINKPAD-X280
  USERDOMAIN_ROAMINGPROFILE=THINKPAD-X280
  USERNAME=Win10
  USERPROFILE=C:\Users\Win10

さらに、本書の「付録 A WinDbg の Tips」で紹介している MEX 拡張機能4!mex.ver コマンドや !mex.computername コマンドを使用することでも、システムの OS 情報やコンピュータ名の情報を収集できます。

# MEX 拡張機能による OS のバージョン情報の確認
0: kd> !mex.ver
Platform ID: 2
Major Version: 10
Minor Version: 0
WinXP: False
Win2K3: False
Win2k3SP1OrNewer: True
Vista: False
VistaOrNewer: True
Win7: False
Win8: False
Blue: False
19041.1.amd64fre.vb_release.191206-1406
Build Number: 19041
Kernel Start Address: ffff800000000000
System Version Build String: 19041.1.amd64fre.vb_release.191206-1406

# MEX 拡張機能によるコンピュータ名の確認
0: kd> !mex.computername
Computer Name: THINKPAD-X280

システムのレジストリ情報を探索する

Windows システムの場合、OS やアプリケーションの設定や、その他の様々な情報はレジストリに保存されています。

そのため、フルメモリダンプを解析してレジストリハイブを探索することで、システムや設定に関するほとんどの情報にアクセスできます。

WinDbg でフルメモリダンプからレジストリの検索を行うためには !reg 拡張機能4を使用します。

例えば、以下のステップでコマンドを実行することで !reg 拡張機能を使用してフルメモリダンプから指定のレジストリ値を探索できます。

  1. !reg hivelist コマンドを使用して、システム内のレジストリハイブのアドレスを取得する
  2. !reg openkeys <ハイブのアドレス> コマンドを使用して、正確なハイブ名やキー制御ブロック(KCB)のアドレスを取得する
  3. !reg querykey <ハイブ名> コマンドを使用して、サブキーのアドレス情報を取得する
  4. !reg keyinfo <ハイブのアドレス> <サブキーのアドレス> コマンドを使用してサブキー内のキーとレジストリ値を取得する

このコマンドを使用して、実際に今回の解析対象のフルメモリダンプから適当なレジストリの情報を取得してみます。

まずは、!reg hivelist コマンドでシステム内のレジストリハイブの情報を列挙します。

レジストリハイブの列挙

画像の縮尺だと読みづらいかとは思いますが、出力結果の 7 行目に FileName が emRoot\System32\Config\SOFTWARE となっているハイブの情報が表示されています。(FileName の列は字数制限でパスの末尾 32 文字しか表示されません。)

これは、既定の SOFTWARE ハイブのパスである %SystemRoot%\System32\Config\SOFTWARE と一致します。5

つまり、HKEY_LOCAL_MACHINE\SOFTWARE と対応するレジストリを探索する場合は、7 行目の HiveAddr に表示されているアドレス 0xffffa58428b62000 を使用すればよいことがわかります。

次に、取得した SOFTWARE ハイブの HiveAddr を使用して !reg openkeys ffffa58428b62000 コマンドを実行します。

!reg openkeys コマンドは引数無しでも実行できますが、出力結果が非常に多くなるため解析対象のハイブアドレスを指定しています。

0: kd> !reg openkeys ffffa58428b62000

Hive: \REGISTRY\MACHINE\SOFTWARE
===========================================================================================
Index 0: 	 00000000 kcb=ffffa5842b2d1d50 cell=00000020 f=002c0000 \REGISTRY\MACHINE\SOFTWARE
Index 1: 	 f386608f kcb=ffffa584316e1d00 cell=017c4288 f=00200000 \REGISTRY\MACHINE\SOFTWARE\SYNAPTICS\SYNTPENH\ZONECONFIG\DEFAULTS\PALMCHECK GROUP\2FVSCROLL ZONE
		 8a10ced9 kcb=ffffa5842d346350 cell=80008f90 f=00200000 \REGISTRY\MACHINE\SOFTWARE\MICROSOFT\WINDOWS\CURRENTVERSION\APPMODEL\STATEREPOSITORY\CACHE\PACKAGEEXTERNALLOCATION
Index 2: 	 5c8055db kcb=ffffa58433820390 cell=002a8cf8 f=00200000 \REGISTRY\MACHINE\SOFTWARE\CLASSES\CLSID\{896664F7-12E1-490F-8782-C0835AFD98FC}\INSTANCE
{{ 省略 }}

実際にこのコマンドを実行してみると、上記のように出力結果の最初の行に Hive: \REGISTRY\MACHINE\SOFTWARE と表示されることを確認できます。

そのため、この結果を使用して !reg querykey \REGISTRY\MACHINE\SOFTWARE コマンドを実行します。(ある程度解析に慣れている場合は、初めからこのコマンドを実行しても問題ありません。)

このコマンドの出力結果は以下のようになりました。

ここまでで特定したハイブアドレスの 0xffffa58428b62000 や、キー制御ブロック(KCB)のアドレス、さらにサブキーの一覧とともに SubKeyAddr が表示されていることがわかります。

0: kd> !reg querykey \REGISTRY\MACHINE\SOFTWARE

Found KCB = ffffa5842b2d1d50 :: \REGISTRY\MACHINE\SOFTWARE

Hive         ffffa58428b62000
KeyNode      0000022eaab61024

[SubKeyAddr]         [SubKeyName]
22eaab61174          Classes
22eab25daec          Clients
22eab5b2184          CVSM
22eab5b244c          DefaultUserEnvironment
22eab5b2624          Dolby
22eab5b297c          Fortemedia
22eab5b2b0c          Google
22eab5b2dec          InstalledOptions
22eab5b2e4c          Intel
22eab5b7af4          JavaSoft
22eab5b7b4c          Lenovo
22eab5b8524          Microsoft
22eac3103ac          Mozilla
22eac310614          Nuance
22eac31066c          ODBC
22eac3106c4          OEM
22eac310894          OpenSSH
22eac31097c          Oracle
22eac3109d4          Partner
22eac310a2c          Policies
22eac313854          Realtek
22eac313d34          RegisteredApplications
22eac3144bc          SRS Labs
22eac31467c          Synaptics
22eac32d2c4          Windows
22eac32d31c          WOW6432Node

 Use '!reg keyinfo ffffa58428b62000 <SubKeyAddr>' to dump the subkey details

[ValueType]         [ValueName]                   [ValueData]
 Key has no Values

これで必要な情報を得ることができたので、最後に、!reg keyinfo <ハイブのアドレス> <サブキーのアドレス> コマンドを使用してサブキー内のキーとレジストリ値を取得します。

例えば、SOFTWARE ハイブ内で SubKeyAddr が 0x22eab5b8524 であるサブキー Microsoft の探索を行う場合のコマンドは !reg keyinfo ffffa58428b62000 22eab5b8524 になります。

このコマンドを実行してみると、以下のように HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft 内のサブキーの一覧と SubKeyAddr が表示されます。

0: kd> !reg keyinfo ffffa58428b62000 22eab5b8524

KeyPath 	\REGISTRY\MACHINE\SOFTWARE\Microsoft

[SubKeyAddr]         [SubKeyName]
22eab5b8694          .NETFramework
22eab5fc13c          AccountsControl
22eab5fc19c          Active Setup
22eab5ff7dc          ActiveSync
22eab6009ac          ADs
22eab600a04          Advanced INF Setup
22eab600a6c          ALG
22eab600ce4          AllUserInstallAgent
22eab600e94          AMSI
・・・
22eab79a0ec          Windows
22eac107024          Windows Advanced Threat Protection
22eac1073ac          Windows Defender
22eac10bcf4          Windows Defender Security Center
・・・

さらに深い階層のレジストリ情報を探索したい場合は、上記で取得した SubKeyAddr を指定して再度 !reg keyinfo コマンドを発行します。

例えば、SubKeyAddr が 0x22eac1073ac であるサブキー Windows Defender の情報を取得する場合、実行するコマンドは !reg keyinfo ffffa58428b62000 22eac1073ac になります。

実際にこのコマンドを実行してみると、出力結果の上部に HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Defender 内のサブキーの一覧が表示され、下部にこのレジストリキー内に存在する値の一覧が表示されます。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Defender のレジストリ情報

これで、フルメモリダンプからシステム内のレジストリ情報を探索することができました。

メモリリソース使用情報を調査する

ここまでの項では、ハードウェアや OS の構成情報を収集しました。

続いては、フルメモリダンプ取得時(システムクラッシュ発生時)のメモリリソースの使用状況を調査します。

このような情報は特に、今回のようなメモリリーク事象などのパフォーマンス系の問題を調査する際に役に立ちます。

まずは、システム内の仮想メモリの使用状況を調査可能な !vm 拡張機能6を使用します。

!vm コマンドをオプション引数無しで実行すると、システム全体の仮想メモリ使用量に関する統計情報とプロセスごとのコミットサイズに関する情報を取得できます。

今回の解析対象のフルメモリダンプをロードして !vm コマンドを実行することで、D4C.exe のプロセスがシステム内で最も仮想メモリ領域を消費しており、ユーザモードメモリリークが発生している可能性が高いことを特定できます。

プロセスごとの仮想メモリ使用量

仮想メモリのリソース消費状況を確認できたので、次は物理メモリのリソース消費量を調査してみます。

物理メモリのリソース消費量を調査するには !memusage 拡張機能7を使用できます。

!memusage 拡張機能は、Windows システムが物理メモリの管理に使用するページフレーム番号(PFN)データベースの情報を使用することで物理メモリの統計情報を出力します。

!memusage コマンドの出力結果は非常に膨大になるので、今回は概要情報のみを表示する !memusage 0x08 コマンドを実行します。

0: kd> !memusage 0x08
loading PFN database
loading (100% complete)
Compiling memory usage data (99% Complete).
          Zeroed:   319761 ( 1279044 kb)
            Free:      460 (    1840 kb)
         Standby:  1669387 ( 6677548 kb)
        Modified:    50530 (  202120 kb)
 ModifiedNoWrite:        7 (      28 kb)
    Active/Valid:  1118305 ( 4473220 kb)
      Transition:  1002519 ( 4010076 kb)
      SLIST/Temp:     6687 (   26748 kb)
             Bad:        0 (       0 kb)
         Unknown:        0 (       0 kb)
           TOTAL:  4167656 (16670624 kb)

Dangling Yes Commit:      169 (     676 kb)
 Dangling No Commit:    50768 (  203072 kb)

このコマンドを実行すると、上記の通りシステムの物理メモリ使用量に関する統計情報を取得できます。

出力結果の各項目は PFN データベースに含まれる物理ページの状態に関する情報と対応しています。

よく参照する一部の項目の概要について以下に記載します。8

  • Zeroed:ゼロで初期化されているか、既にゼロであることが明らかな空きページ
  • Free:空きページだが、ゼロで初期化されていないページ
  • Standby:過去にワーキングセットに登録されていたが、現在はスタンバイページリストに追加されているページ
  • Active または Valid:ワーキングセットの一部であるページや、非ページカーネルページ
  • Transition:ワーキングセットや他のページリストにも存在しない一時的なページ(そのページに対して I/O 処理が行われている場合などに発生)
  • Bad:ハードウェアエラーにより読み取り不可となったページ

PFN データベースに関してより詳しく知りたい場合は、参考情報に記載している「インサイド Windows 第 7 版 上」がおすすめです。

稼働中のプロセスの情報を調査する

次は、システム内で稼働しているプロセスに関する情報を収集します。

稼働中のプロセスに関する情報は、適切なプロセスコンテキストの設定や CPU 使用率の高騰などの問題の調査など、様々な場面で使用します。

プロセスの情報収集に使用できる方法は複数ありますが、本書では一部のみ紹介します。

まずは、!process 拡張機能9を使用してみます。

システムのフルメモリダンプなど、EPROCESS 構造体の情報を含むカーネル空間のメモリ情報を WinDbg にロードして !process 0 0 コマンドを実行することで、システム内のすべてのプロセスの概要情報を列挙できます。

このコマンドの出力結果には以下のように EPROCESS 構造体のアドレスとプロセス名が含まれます。

そのため、このコマンドで取得した EPROCESS 構造体のアドレスを使用してプロセスのコンテキストを変更したり、対象プロセスのより詳細な情報を取得したりできるようになります。

0: kd> !process 0 0

**** NT ACTIVE PROCESS DUMP ****
PROCESS ffffcb0c8a6bf080
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad002  ObjectTable: ffffa58427e2e600  HandleCount: 3952.
    Image: System

PROCESS ffffcb0c8a71f080
    SessionId: none  Cid: 007c    Peb: 00000000  ParentCid: 0004
    DirBase: 007dc002  ObjectTable: ffffa58427e5c5c0  HandleCount:   0.
    Image: Registry

{{ 省略 }}

また、すでに調査対象のプロセス名を特定できている場合は、!process 0 0 <プロセス名> とすることで、特定のプロセスのプロセスオブジェクト(EPROCESS)のアドレスを検索することも可能です。

実際に D4C.exe を指定して !process 0 0 D4C.exe コマンドを実行してみると、D4C.exe のプロセスオブジェクトがアドレス 0xffffcb0c950ea0c0 に存在することを特定できました。(このアドレスは後ほど使用します。)

0: kd> !process 0 0 D4C.exe

PROCESS ffffcb0c950ea0c0
    SessionId: 3  Cid: 0d40    Peb: 13c75e4000  ParentCid: 3758
    DirBase: 27cd1f002  ObjectTable: ffffa5843cdd8c00  HandleCount:  51.
    Image: D4C.exe

ちなみに、!process コマンドの 1 つ目の引数にプロセスオブジェクトのアドレスを与えることでも、特定のプロセスの情報を出力させることが可能です。(0 が指定されている場合はすべてのアクティブなプロセスの情報が表示されます)

そして、2 つ目の引数は表示する情報に関する Flag を意味しており、0 が指定された場合は最小限の情報のみが出力されます。

一方で、2 つ目の引数に 7 を指定した場合は、プロセスに関連するスレッドやスタックバックトレースを含む詳細情報を表示できます。

そのため、解析対象のプロセス名が特定できている場合には、!process 0 7 D4C.exe のようなコマンドを実行することでより詳細な情報を収集できます。

D4C.exe プロセスの詳細情報の出力

!for_each_process 拡張機能10も、オプション引数なしで実行した場合には !process 0 0 と同等の情報を出力します。

ただし、システム内でアクティブなすべてのプロセスに対して任意のデバッガコマンドを発行できる点が !process 拡張機能との違いです。

例えば、!for_each_process ".echo @#Process" コマンドを実行すると、すべてのプロセスのプロセスオブジェクトのアドレスを出力できます。(!for_each_process のコマンド文字列内では、@#Process が自動的にプロセスオブジェクトのアドレスに置換されます。)

そのため、!for_each_process ".process /r /p @#Process; lm" のようなコマンドを実行すると、すべてのアクティブなプロセスにプロセスコンテキストを変更して lm コマンドを実行する、といった操作が可能になります。

さらに高度な応用として !for_each_process ".process @#Process; dt ntdll!_EPROCESS @#Process Peb->ProcessParameters->CommandLine" のようなコマンドを使用できます。

これは、!for_each_process で取得したすべてのプロセスの EPROCESS 構造体から PEB の情報を取得し、コマンドラインの情報を列挙しています。

このように、!for_each_process はプロセスの列挙を行う際に非常に柔軟なコマンドを発行できます。

また、システム内のプロセスを列挙する場合には、MEX 拡張機能に含まれる !mex.tlist なども非常に便利です。

!mex.tlist コマンドを実行すると、以下のように PID やプロセスオブジェクトのアドレス、そしてプロセス名をフォーマットして出力できます。

!mex.tlist によるプロセス一覧の取得

他にも、MEX 拡張機能に含まれる非常に便利なコマンドとして !mex.commandline -a があります。

これは、システム内でアクティブなすべてのプロセスのコマンドラインを列挙できるコマンドです。

このコマンドを実行することで、以下のようにすべてのプロセスのアドレスと実行時のコマンドラインの情報を出力できます。

プロセスのコマンドライン情報の列挙

WinDbg でプロセスの一覧を取得する方法は他にもありますが、基本的には上記のいずれかを使用しておけば困ることはないでしょう。

次に、特定のプロセスにプロセスコンテキストを変更した上でより詳細な情報を参照していきます。

プロセスコンテキストの変更は .process /r /p <プロセスオブジェクトのアドレス> で行うことが可能です。

先ほど特定した D4C.exe のプロセスにコンテキストを変更する場合は .process /r /p 0xffffcb0c950ea0c0 コマンドを実行します。

プロセスコンテキストの変更に成功したかどうかを確認するには、!peblm などのコマンドを実行してみるとよいでしょう。

これらのコマンドはデバッガのプロセスコンテキストに基づいて情報を出力するため、D4C.exe に関する情報が出力された場合には、プロセスのコンテキストを正常に変更できたと判断できます。

プロセスコンテキストの変更

前述の通りプロセスのコマンドを変更することで、ユーザモードプロセスダンプを解析した際と同じように !peblm コマンドなどによってプロセスの詳細情報を調査することができるようになります。

特定のプロセスのスタックバックトレースを調査する

システムのプロセス情報を列挙してデバッガのプロセスコンテキストを変更することができたので、次は特定のプロセスのスタックバックトレースの情報を取得します。

すでに !process 0 7 D4C.exe コマンドでプロセスのすべてのスレッドのスタックバックトレースを出力可能であることは前項で確認済みですが、ここではあえて k コマンドによる情報取得を実施します。

しかし、前項までの手順でデバッガのプロセスコンテキストを D4C.exe に設定しても、実行スレッドのスタックバックトレースを出力する k コマンドは D4C.exe の情報を出力しません。

これは、k コマンドはデバッガのレジスタコンテキストに依存しているためです。11

そのため、まずは .thread コマンド12を使用してデバッガのレジスタコンテキストを変更します。

.thread コマンドでレジスタコンテキストを変更するためには、変更対象のスレッドのアドレスを指定する必要があります。

システムのフルメモリダンプからプロセスのスレッドを探索する方法はいくつかありますが、前項で紹介した !process 拡張機能を使用する方法が簡単です。

!process 拡張機能でプロセスのスレッド情報を表示するには、オプション引数の Flag に 2 を指定します。(すべての情報を出力できる 7 を使用することも可能です)

!process 拡張機能で D4C.exe のプロセスのスレッド情報を取得する場合、!process 0 2 D4C.exe コマンドを実行します。

D4C.exe のスレッド情報を表示

これによって、D4C.exe のプロセスと紐づく 3 つのスレッドのアドレスがそれぞれ 0xffffcb0c93f24080 と 0xffffcb0c93d17080、そして 0xffffcb0c95645080 であることを特定できました。

これで、特定した 3 つのアドレスを使用してスレッドコンテキストを設定することで、k コマンドにより D4C.exe のスレッドのスタックバックトレースの情報を参照できるようになります。

# 1 つめのスレッドコンテキストを設定してスタックバックトレースを出力
0: kd> .thread 0xffffcb0c93f24080; k
Implicit thread is now ffffcb0c`93f24080
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 ffffef81`2c6ef5e0 fffff806`72e1bca0     nt!KiSwapContext+0x76
01 ffffef81`2c6ef720 fffff806`72e1b1cf     nt!KiSwapThread+0x500
02 ffffef81`2c6ef7d0 fffff806`72e1aa73     nt!KiCommitThreadWait+0x14f
03 ffffef81`2c6ef870 fffff806`73201b11     nt!KeWaitForSingleObject+0x233
04 ffffef81`2c6ef960 fffff806`73201a6a     nt!ObWaitForSingleObject+0x91
{{ 省略 }}

# 2 つめのスレッドコンテキストを設定してスタックバックトレースを出力
0: kd> .thread 0xffffcb0c93d17080; k
Implicit thread is now ffffcb0c`93d17080
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 ffffef81`2c7476e0 fffff806`72e1bca0     nt!KiSwapContext+0x76
01 ffffef81`2c747820 fffff806`72e1b1cf     nt!KiSwapThread+0x500
02 ffffef81`2c7478d0 fffff806`72ef51a4     nt!KiCommitThreadWait+0x14f
03 ffffef81`2c747970 fffff806`732b1d20     nt!KeWaitForAlertByThreadId+0xc4
04 ffffef81`2c7479d0 fffff806`730105f5     nt!NtWaitForAlertByThreadId+0x30
{{ 省略 }}

# 3 つめのスレッドコンテキストを設定してスタックバックトレースを出力
0: kd> .thread 0xffffcb0c95645080; k
Implicit thread is now ffffcb0c`95645080
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 ffffef81`2c847310 fffff806`72e1bca0     nt!KiSwapContext+0x76
01 ffffef81`2c847450 fffff806`72e1b1cf     nt!KiSwapThread+0x500
02 ffffef81`2c847500 fffff806`72e1aa73     nt!KiCommitThreadWait+0x14f
03 ffffef81`2c8475a0 fffff806`72ff1494     nt!KeWaitForSingleObject+0x233
04 ffffef81`2c847690 fffff806`732011ab     nt!IopWaitForSynchronousIoEvent+0x50
{{ 省略 }}

さらに、スレッドオブジェクトのアドレスを !thread 拡張機能13の引数として与えることで、スレッドコンテキストを変更しなくても対象スレッドの実行時間やスタックバックトレースをまとめて表示できます。

以下は、!thread 0xffffcb0c93d17080 コマンドを実行した場合の出力結果です。

!thread 拡張機能の出力結果

ちなみに、3 章で触れた通り、Windows のスレッドは ETHREAD 構造体として表現されており、その最初のメンバである KTHREAD 構造体の中に TEB(スレッド環境ブロック) が含まれています。

つまり、ここで取得したスレッドオブジェクトのアドレスを使用して dt ntdll!_ETHREAD <スレッドオブジェクトのアドレス> Tcb->Teb コマンドを実行することで特定のスレッドオブジェクトの TEB のアドレスを簡単に参照できます。

取得した TEB のアドレスを !teb 拡張機能の引数として与えることで、対象の TEB の情報をデバッガで出力することが可能になります。

0: kd> dt ntdll!_ETHREAD 0xffffcb0c93f24080 Tcb->Teb
   +0x000 Tcb      : 
      +0x0f0 Teb      : 0x00000013`c75e5000 Void

0: kd> !teb 0x00000013c75e5000
TEB at 00000013c75e5000
  ExceptionList:        0000000000000000
  StackBase:            00000013c7360000
  StackLimit:           00000013c735c000
  SubSystemTib:         0000000000000000
  FiberData:            0000000000001e00
  ArbitraryUserPointer: 0000000000000000
  Self:                 00000013c75e5000
  EnvironmentPointer:   0000000000000000
  ClientId:             0000000000000d40 . 00000000000029c8
  RpcHandle:            0000000000000000
  Tls Storage:          000001244e6d33c0
  PEB Address:          00000013c75e4000
  LastErrorValue:       0
  LastStatusValue:      c0000034
  Count Owned Locks:    0
  HardErrorMode:        0

ユーザモードプロセスのヒープ情報を調査する

解析対象がシステムのフルメモリダンプの場合でも、デバッガのプロセスコンテキストを適切に設定することで、!heap 拡張機能によるプロセスヒープの情報を参照できるようになります。

0: kd> .process /r /p 0xffffcb0c950ea0c0

0: kd> !heap
Heap Address      NT/Segment Heap
 1244e6d0000       NT Heap
 1244e500000       NT Heap
 1244e950000       NT Heap

0: kd> !heap -a
HEAPEXT: Unable to get address of ntdll!RtlpHeapInvalidBadAddress.
Index   Address  Name      Debugging options enabled
  1:   1244e6d0000 
    Segment at 000001244e6d0000 to 000001244e7cf000 (000ef000 bytes committed)
    Segment at 000001244e810000 to 000001244e90f000 (000f8000 bytes committed)
    Segment at 000001244e960000 to 000001244eb5f000 (001f8000 bytes committed)
    Segment at 000001244eb60000 to 000001244ef5f000 (003f7000 bytes committed)
{{ 省略 }}

!heap コマンドによるユーザモードヒープの調査方法は 6 章と同じですので、ここでは割愛します。

6 章と同じように適当なヒープセグメント内のヒープエントリをいくつかダンプしてみると、以下のように ==> Allocated addr: から始まる文字列が書き込まれていることを確認できました。

ヒープ領域のメモリ情報をダンプ

ここから、6 章と同じ手順で Ghidra デコンパイラを使用してヒープへの書き込みを行っている処理を調べることで、ユーザモードメモリリークの原因箇所を特定できます。

ちなみに、本書では使用しませんが、ユーザモードではなくカーネルモードのメモリリーク事象を調査する場合は、!pool 拡張機能14!poolused 拡張機能15などを使用してページプールや非ページプールの情報を確認します。

7 章のまとめ

6 章と 7 章では、クラッシュを伴わない問題の原因をダンプファイルから調査する例として、ユーザモードアプリケーションのメモリリーク事象の解析を行いました。

プロセスダンプの解析とは異なり、システムのフルメモリダンプから特定のプロセスのトラブルシューティングを行う場合には、適切なプロセスコンテキストやレジスタコンテキストをデバッガに設定する必要があります。

この点は、はじめてダンプファイルを解析する方にとって一つのハードルになりやすい点かと思いますので、6 章の解析と比較しながらお読みいただければと思います。

なお、本書ではクラッシュを伴わない問題としてユーザアプリケーションのメモリリーク事象を扱いましたが、このような問題は他にいくつもあります。

例えば、CPU 使用率の高騰やプロセスのハングアップ、他にもアプリケーションのデッドロックやハンドルリーク、さらにはシステムのメモリプールの枯渇といった問題も、ダンプファイルを解析することで原因調査が可能です。

これらの問題を調査する場合には、クラッシュやメモリリークとはまた違ったアプローチでのダンプファイルの解析を楽しむことができます。

Vol.1 では残念ながらこれらの解析手法を紹介することができませんが、Windows のダンプファイル解析をさらに深く学びたい方は、「Welcome to WinDbg.info」の Crash Me や、NotMyFault などを使用して様々なトラブルを再現し、ダンプファイルを取得して解析を行うことをおすすめします。


Crash Me:

http://windbg.info/


各章へのリンク


  1. バグチェック 0xE2:MANUALLY_INITIATED_CRASH https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/bug-check-0xe2—manually-initiated-crash

  2. !sysinfo 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-sysinfo

  3. !cpuinfo 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-cpuinfo

  4. MEX 拡張機能 https://www.microsoft.com/en-us/download/details.aspx?id=53304

  5. !reg 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-reg

  6. 上級ユーザー向けの Windows レジストリ情報 https://learn.microsoft.com/ja-jp/troubleshoot/windows-server/performance/windows-registry-advanced-users

  7. !vm 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-vm

  8. !memusage 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-memusage

  9. インサイド Windows 第 7 版 上 P.471 (Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon 著 / 山内 和朗 訳 / 日系 BP 社 / 2018 年)

  10. !process 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-process

  11. !foreachprocess 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-for-each-process

  12. k、kb、kc、kd、kp、kP、kv スタック バックトレースの表示 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/k—kb—kc—kd—kp—kp—kv—display-stack-backtrace-

  13. .thread レジスタコンテキストの設定 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-thread—set-register-context-

  14. !thread 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-thread

  15. !pool 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-pool

  16. !poolused 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-poolused