本書の最後の章である 7 章では、6 章で解析したものと同じユーザモードのメモリリーク事象をシステムのフルメモリダンプから解析していきます。
メモリリーク事象の調査方法自体は 6 章と同じですので、7 章はどちらかというとフルメモリダンプから様々な情報を抜き出すテクニックの紹介が中心となります。
しかし、カーネルモードのメモリ情報を含むフルメモリダンプを解析する場合には、ユーザモードのプロセスダンプ解析と比較して使用可能なコマンドや出力結果が異なるため、また違ったアプローチでの解析を楽しむことができると思います。
もくじ
- システムのフルメモリダンプの取得
- WinDbg にフルメモリダンプをロードする
- 端末のハードウェア情報を収集する
- システム情報を収集する
- システムのレジストリ情報を探索する
- メモリリソース使用情報を調査する
- 稼働中のプロセスの情報を調査する
- 特定のプロセスのスタックバックトレースを調査する
- ユーザモードプロセスのヒープ情報を調査する
- 7 章のまとめ
- 各章へのリンク
システムのフルメモリダンプの取得
解析用のフルメモリダンプを取得するため、6 章の手順と同じく、D4C.exe を起動し、2 番のメニューを選択してアプリケーションのメモリリーク事象を再現します。
Process Explorer などのツールを使用して D4C.exe のプロセスが使用する仮想メモリ領域が肥大化したことを確認したら、キーボード操作によってシステムクラッシュを発生させることでフルメモリダンプを取得します。
キーボード操作によるシステムクラッシュは、1 章と同じ手順でキーボードの右 Ctrl キーを押しながら、Space キーを 2 回連打することで引き起こすことが可能です。
1 章で実施したキーボードクラッシュの設定が反映されている場合、上記のキー操作を実施するとシステムがクラッシュし、ブルースクリーン画面が表示されます。
システムの再起動後、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
コマンドをオプション無しで実行すると、すべてのプロセッサの情報が表示されます。
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
拡張機能を使用してフルメモリダンプから指定のレジストリ値を探索できます。
!reg hivelist
コマンドを使用して、システム内のレジストリハイブのアドレスを取得する!reg openkeys <ハイブのアドレス>
コマンドを使用して、正確なハイブ名やキー制御ブロック(KCB)のアドレスを取得する!reg querykey <ハイブ名>
コマンドを使用して、サブキーのアドレス情報を取得する!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
内のサブキーの一覧が表示され、下部にこのレジストリキー内に存在する値の一覧が表示されます。
これで、フルメモリダンプからシステム内のレジストリ情報を探索することができました。
メモリリソース使用情報を調査する
ここまでの項では、ハードウェアや 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
のようなコマンドを実行することでより詳細な情報を収集できます。
!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 拡張機能に含まれる非常に便利なコマンドとして !mex.commandline -a
があります。
これは、システム内でアクティブなすべてのプロセスのコマンドラインを列挙できるコマンドです。
このコマンドを実行することで、以下のようにすべてのプロセスのアドレスと実行時のコマンドラインの情報を出力できます。
WinDbg でプロセスの一覧を取得する方法は他にもありますが、基本的には上記のいずれかを使用しておけば困ることはないでしょう。
次に、特定のプロセスにプロセスコンテキストを変更した上でより詳細な情報を参照していきます。
プロセスコンテキストの変更は .process /r /p <プロセスオブジェクトのアドレス>
で行うことが可能です。
先ほど特定した D4C.exe のプロセスにコンテキストを変更する場合は .process /r /p 0xffffcb0c950ea0c0
コマンドを実行します。
プロセスコンテキストの変更に成功したかどうかを確認するには、!peb
や lm
などのコマンドを実行してみるとよいでしょう。
これらのコマンドはデバッガのプロセスコンテキストに基づいて情報を出力するため、D4C.exe に関する情報が出力された場合には、プロセスのコンテキストを正常に変更できたと判断できます。
前述の通りプロセスのコマンドを変更することで、ユーザモードプロセスダンプを解析した際と同じように !peb
や lm
コマンドなどによってプロセスの詳細情報を調査することができるようになります。
特定のプロセスのスタックバックトレースを調査する
システムのプロセス情報を列挙してデバッガのプロセスコンテキストを変更することができたので、次は特定のプロセスのスタックバックトレースの情報を取得します。
すでに !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 のプロセスと紐づく 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
コマンドを実行した場合の出力結果です。
ちなみに、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:
各章へのリンク
- まえがき
- 1 章 環境構築
- 2 章 WinDbg の基本操作
- 3 章 解析に必要な前提知識
- 4 章 アプリケーションのクラッシュダンプを解析する
- 5 章 システムクラッシュ時のフルメモリダンプを解析する
- 6 章 プロセスダンプからユーザモードアプリケーションのメモリリーク事象を調査する
- 7 章 フルメモリダンプからユーザモードメモリリーク事象を調査する
- 付録 A WinDbg の Tips
- 付録 B Volatility 3 でクラッシュダンプを解析する
-
バグチェック
↩0xE2:MANUALLY_INITIATED_CRASH
https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/bug-check-0xe2—manually-initiated-crash -
!sysinfo 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-sysinfo
↩ -
!cpuinfo 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-cpuinfo
↩ -
MEX 拡張機能 https://www.microsoft.com/en-us/download/details.aspx?id=53304
↩ -
!reg 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-reg
↩ -
上級ユーザー向けの Windows レジストリ情報 https://learn.microsoft.com/ja-jp/troubleshoot/windows-server/performance/windows-registry-advanced-users
↩ -
!vm 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-vm
↩ -
!memusage 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-memusage
↩ -
インサイド Windows 第 7 版 上 P.471 (Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon 著 / 山内 和朗 訳 / 日系 BP 社 / 2018 年)
↩ -
!process 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-process
↩ -
!foreachprocess 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-for-each-process
↩ -
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-
↩ -
.thread レジスタコンテキストの設定 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-thread—set-register-context-
↩ -
!thread 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-thread
↩ -
!pool 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-pool
↩ -
!poolused 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-poolused
↩