kernel-mode memusage

Vista kernel の直接観測

ホワイトペーパー: Windows のメモリ管理の進歩』のちょっとした続き.
予習は OK ということで,カーネルデバッガを利用してより直接的に変更点を見てみることにします.

おさらい

インサイド Microsoft Windows 第4版上』を読まれた方にはおなじみでしょうが,Windows 環境で,真の意味での物理メモリの使用状況を見るには,Windbg で !memusage コマンドを実行するのが手っ取り早い方法です.
!memusage コマンドは,Page Frame Number (PFN) database のエントリをひとつひとつスキャンし,物理ページが何の用途に使われているかを集計します.

0: kd> !memusage
 loading PFN database
loading (100% complete)
Compiling memory usage data (99% Complete).
             Zeroed: 173816 (695264 kb)
               Free:      0 (     0 kb)
            Standby:  71317 (285268 kb)
           Modified:   1387 (  5548 kb)
    ModifiedNoWrite:    561 (  2244 kb)
       Active/Valid: 145881 (583524 kb)
         Transition:     16 (    64 kb)
            Unknown:      0 (     0 kb)
              TOTAL: 392978 (1571912 kb)

メモリが 1.5 GB あれば,約 1.5 GB の内訳が表示されますし,2.0 GB なら約 2.0 GB の内訳が表示されるでしょう.
タスクマネージャにこれらの値が表示されれば楽なのですが,カーネルデバッガを使わなければこの真の値が分からないところが事態をややこしくしています.
!memusage コマンドは,上の表示に続けて,どのファイルが何バイトメモリ上にキャッシュされているかも表示してくれます.カーネルデバッガを使えば,SuperFetch によってメモリにプリロードされたファイルらしきものを調べることもできるわけです.(id:NyaRuRu:20060918:p1)
さて,この !memusage の結果ですが,カーネルデバッガなしになんとか取得できないでしょうか? 以前 (id:NyaRuRu:20060529) でこの問題に挑戦しましたが,ちょっとばかりその続きを書いておきます.

_MMPFN 構造体

前回も書いたように (id:NyaRuRu:20060529),物理メモリの使用状況を調べるには,PFN データベースの各エントリの内容を調べていくことになります.
PFN データベースのメモリレイアウトは,Windbg の dt コマンドを使えばダンプできます.以下は Windows Vista x86 SP1 (PAE disabled) の _MMPFN 構造体の内容です.

lkd> dt -r nt!_MMPFN
   +0x000 u1               : 
      +0x000 Flink            : Uint4B
      +0x000 WsIndex          : Uint4B
      +0x000 Event            : Ptr32 _KEVENT
         +0x000 Header           : _DISPATCHER_HEADER
      +0x000 Next             : Ptr32 Void
      +0x000 VolatileNext     : Ptr32 Void
      +0x000 KernelStackOwner : Ptr32 _KTHREAD
         +0x000 Header           : _DISPATCHER_HEADER
         (中略)
      +0x000 NextStackPfn     : _SINGLE_LIST_ENTRY
         +0x000 Next             : Ptr32 _SINGLE_LIST_ENTRY
   +0x004 u2               : 
      +0x000 Blink            : Uint4B
      +0x000 ImageProtoPte    : Ptr32 _MMPTE
         +0x000 u                : 
      +0x000 ShareCount       : Uint4B
   +0x008 PteAddress       : Ptr32 _MMPTE
      +0x000 u                : 
         +0x000 Long             : Uint4B
         +0x000 VolatileLong     : Uint4B
         +0x000 Flush            : _HARDWARE_PTE
         +0x000 Hard             : _MMPTE_HARDWARE
         +0x000 Proto            : _MMPTE_PROTOTYPE
         +0x000 Soft             : _MMPTE_SOFTWARE
         +0x000 TimeStamp        : _MMPTE_TIMESTAMP
         +0x000 Trans            : _MMPTE_TRANSITION
         +0x000 Subsect          : _MMPTE_SUBSECTION
         +0x000 List             : _MMPTE_LIST
   +0x008 VolatilePteAddress : Ptr32 Void
   +0x00c u3               : 
      +0x000 ReferenceCount   : Uint2B
      +0x002 e1               : _MMPFNENTRY
         +0x000 PageLocation     : Pos 0, 3 Bits
         +0x000 WriteInProgress  : Pos 3, 1 Bit
         +0x000 Modified         : Pos 4, 1 Bit
         +0x000 ReadInProgress   : Pos 5, 1 Bit
         +0x000 CacheAttribute   : Pos 6, 2 Bits
         +0x001 Priority         : Pos 0, 3 Bits
         +0x001 Rom              : Pos 3, 1 Bit
         +0x001 InPageError      : Pos 4, 1 Bit
         +0x001 KernelStack      : Pos 5, 1 Bit
         +0x001 RemovalRequested : Pos 6, 1 Bit
         +0x001 ParityError      : Pos 7, 1 Bit
      +0x000 e2               : 
         +0x000 ReferenceCount   : Uint2B
         +0x000 VolatileReferenceCount : Int2B
         +0x002 ShortFlags       : Uint2B
      +0x000 e3               : 
         +0x000 ReferenceCount   : Uint2B
         +0x002 ByteFlags        : UChar
         +0x003 InterlockedByteFlags : UChar
   +0x010 OriginalPte      : _MMPTE
      +0x000 u                : 
         +0x000 Long             : Uint4B
         +0x000 VolatileLong     : Uint4B
         +0x000 Flush            : _HARDWARE_PTE
         +0x000 Hard             : _MMPTE_HARDWARE
         +0x000 Proto            : _MMPTE_PROTOTYPE
         +0x000 Soft             : _MMPTE_SOFTWARE
         +0x000 TimeStamp        : _MMPTE_TIMESTAMP
         +0x000 Trans            : _MMPTE_TRANSITION
         +0x000 Subsect          : _MMPTE_SUBSECTION
         +0x000 List             : _MMPTE_LIST
   +0x010 AweReferenceCount : Int4B
   +0x014 u4               : 
      +0x000 PteFrame         : Pos 0, 25 Bits
      +0x000 PfnImageVerified : Pos 25, 1 Bit
      +0x000 AweAllocation    : Pos 26, 1 Bit
      +0x000 PrototypePte     : Pos 27, 1 Bit
      +0x000 PageColor        : Pos 28, 4 Bits

注意しなければならないのは,_MMPFN 構造体の内容はカーネルによって異なる点です.実際,PAE (Physical Address Extension) が有効か無効か,32-bit/64-bit,OS のバージョンなどの違いで,_MMPFN 構造体の内容は変化します.
あるいは,_MMPFN 構造体の違いこそ,カーネルごとのメモリ戦略の違いを反映しているとも言えるでしょう.
以下に,手元の Windows XP x86 SP2 (PAE Disabled) と Windows Vista x86 SP1 (PAE Disabled) という環境で調べた _MMPFN 構造体の模式図を示します.




!memusage コマンドを作りたいので,ここでは MMPFNENTRY フィールドに着目します.
MMPFNENTRY はビットフィールドとして定義されており,そのページの状態がどのように分類されているかを示しています.MMPFNENTRY の内容もカーネルによって異なり,カーネルがどのようにメモリ管理を行うかを端的に表すことになります.
以下は先ほど同様 Windows XPWindows Vista での違いを示したものです.

目に付く差異として,例えば,Vista では Priority や KernelStack というフィールドが追加されていることが分かります.
Priority フィールドの用途ですが,SuperFetch や ReadyBoost のために,キャッシュメモリに優先順位を付けたことと関係がありそうです.実際,Windows Vista のパフォーマンスカウンタでは,Standby list が 3 段階のプライオリティに分けられています.
また,KernelStack というフィールドですが,これに対応すると考えられるのが,前回紹介したホワイトペーパーの『Kernel-Mode Stack Jumping in x86 Architectures』の部分です.
以前の Windows では,カーネルモードでの再帰呼び出しによってカーネルスタック (スレッドごとに確保されている) が一度拡張されると,スレッドが終了するまでスタックサイズはそのままでした.これによって仮想アドレス空間が消費されるため,多数のスレッドを使用するプロセスのスケーラビリティを悪化させることになります.
一方,64-bit 版の Windows Server 2003 と,全てのアーキテクチャWindows Vista では,拡張されたカーネルスタックが不要になった段階でアドレス空間を解放するようになりました.これを実装するために,新しくフラグを用意したのかもしれません.

kernel-mode memusage の実装

さて,いよいよ !memusage コマンドの実装について考えます.
『インサイド Microsoft Windows 第4版上』では,PFN データベースの開始アドレスはカーネル変数 MmPfnDatabase に,PFN の個数はカーネル変数 MmNumberOfPhysicalPages にそれぞれ格納されているとあります.しかしこれらのカーネル変数は export されていないため,通常の方法では場所を知ることができません.
このような非公開カーネル変数のアドレスを取得するテクニックについては様々な方法が議論されていますが,有名な方法にカーネルデバッグのサポートのための仕組みを使うというものがあります.
まずカーネルドライバを作成し,MmGetSystemRoutineAddress API を通じて KdSystemDebugControl API のアドレスを取得します *1.この KdSystemDebugControl の,コマンド番号 7 を使用することで,非公開カーネル変数のアドレスが多数格納された DBGKD_GET_VERSION64 構造体を取得することができます.DBGKD_GET_VERSION64 は,WDK に付属する "WDbgExts.h" に最新版が定義されています.

enum SYSDBG_COMMAND
{
    SysDbgGetVersion = 7,
    SysDbgReadMemory = 8,
};

typedef NTSTATUS ( NTAPI * NtSystemDebugControlFunc ) (
    __in SYSDBG_COMMAND Command,
    __in PVOID InputBuffer,
    __in ULONG InputBufferLength,
    __out PVOID OutputBuffer,
    __in ULONG OutputBufferLength,
    __out PULONG ReturnLength,
    __in KPROCESSOR_MODE PreviousMode
    );


NtSystemDebugControlFunc SystemDebugControl = NULL;
UNICODE_STRING  procName;

RtlInitUnicodeString( &procName, L"KdSystemDebugControl" );
SystemDebugControl = (NtSystemDebugControlFunc)MmGetSystemRoutineAddress(&procName);
if( SystemDebugControl == NULL )
{
    // この OS は KdSystemDebugControl をサポートしていない
    SIOCTL_KDPRINT(("IOCTL_PFNSPY_GET_PFN_ADDR_INFO -> STATUS_NOT_SUPPORTED\n"));
}
else
{
    DBGKD_GET_VERSION64 kddata; // "WDbgExts.h" に定義されている
    NTSTATUS ret = SystemDebugControl(
        SysDbgGetVersion,
        NULL,
        0,
        kddata,
        sizeof(DBGKD_GET_VERSION64),
        NULL,
        0 );

#ifdef X86_64
    const _MMPFN* pfnDbBase = (const _MMPFN*) *(VOID**) kddata->MmPfnDatabase;
    // ULONG32 か ULONG64 のどちらかは不明
    condt ULONG32 numPFN = *(ULONG32*) kddata->MmNumberOfPhysicalPages;
     or
    condt ULONG64 numPFN = *(ULONG64*) kddata->MmNumberOfPhysicalPages;
#else
    const _MMPFN* pfnDbBase = (const _MMPFN*) *(VOID**) (ULONG32) kddata->MmPfnDatabase;
    condt ULONG32 numPFN = *(ULONG32*) (ULONG32) kddata->MmNumberOfPhysicalPages;
#endif
}

DBGKD_GET_VERSION64 構造体が取得できれば,目当てのカーネル変数のアドレスが格納されていますので,あとは PFN 配列の開始アドレスからスキャンしていくだけです.

#ifndef X86_64
for( int i = 0; i < numPFN; ++i )
{
   // i 番目の PFN
   // 対応する物理アドレスは  i * PageSize
   const _MMPFN& pfnDbBase = pfnDbBase[i];
}
#else

PFNENTRY の内容に従って,情報を突き詰めていけば,!memusage の代替物が作れることでしょう.
注意としては,先ほども述べたように _MMPFN / PFNENTRY の内容が OS により異なることです.PFNENTRY 正しいメモリレイアウトを把握し,フィールドの正しい解釈を OS の種類ごとに実装する必要があります.このあたりは私もまだちゃんと調査できていないので,興味がある方は挑戦してみて下さい,というお約束の言葉で閉めたいと思います.

今回のオチ

さて,この手法ですが,Windows Vista では OS 起動時にカーネルデバッグを有効にして起動しないと使えません.具体的には,カーネルデバッグを有効にしていないと,KdSystemDebugControl が常に失敗を返すのです.id:NyaRuRu:20071016:p1 で述べたように,カーネルデバッグを有効にした Windows Vista は,常用環境としてはあまりうれしくない代物です.というわけで,結局 KdSystemDebugControl を通じて「便利な !memusage」を作ってもあんまりうれしくない,ということになります.
とはいえ,カーネルドライバによってカーネル空間のメモリの読み出し自体は可能ですから*2,何とかしてカーネル変数 MmPfnDatabase と MmNumberOfPhysicalPages のアドレスを取得できれば (あるいはデバッグモードの時と同じ値が使われていて,それを事前に知ることができれば) ,面白いメモリ使用量チェックツールが作れるかもしれません.

*1:Windows XP SP1 では ZwSystemDebugControl としてユーザーモードからも使用することができました.

*2:[http://openlibsys.org/index-ja.html:title=OpenLibSys] あたりを使えば簡単でしょうかね.