1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windowsメモリ管理の深淵:VirtualAllocから共有メモリ、そしてリソース限界までの徹底検証(前半)

1
Last updated at Posted at 2026-05-07

はじめに

Windowsのメモリ管理は、APIレベルの抽象化(VirtualAlloc等)の裏側で、PTE(Page Table Entry)やPFN(Page Frame Number)といったハードウェアに近いレイヤーが複雑に連動しています。

本記事では、メモリの確保、属性変更、プロセッサ間共有、そしてOSのコミット限界における挙動を、WinDbgを用いたカーネルデバッグを通じて検証した5つのステップをまとめます。
この記事は前半2つのステップとなります。


1. VirtualAllocにおけるWSSとPrivate Usageの解離

まずは基本となる VirtualAlloc の挙動です。MEM_COMMIT と実際の物理メモリ割り当てのタイミングを検証しました。

コード(抜粋)

int main() {
    const size_t allocationSize = 4096 * 100;
    LPVOID p1 = VirtualAlloc(NULL, allocationSize, MEM_RESERVE, PAGE_NOACCESS);
    LPVOID p2 = VirtualAlloc(p1, allocationSize, MEM_COMMIT, PAGE_READWRITE);

    VirtualAlloc(reset_target_page, 4096, MEM_RESET, PAGE_READWRITE);
    VirtualProtect(guard_target_page, 4096, PAGE_READWRITE | PAGE_GUARD, &old);
    VirtualAlloc(read_only_page, 4096, MEM_COMMIT, PAGE_READONLY);
    VirtualAlloc(noaccess_page, 4096, MEM_COMMIT, PAGE_NOACCESS);

    // ワーキングセットから削除(物理メモリから解放)する
    SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
    

    for(int i = 0; i < 5; ++i) {
        __try {
            p[i * 4096] = (BYTE)i; 
        }
        __except (GetExceptionCode() == STATUS_GUARD_PAGE_VIOLATION ? 
              EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
        
            DbgLog("[SUCCESS] STATUS_GUARD_PAGE_VIOLATION caught!\n");
            DbgLog("Exception Code: 0x%x.\n", GetExceptionCode());
        }
        DbgLog("--- After Data Write (Page %d) ---\n", i);
        PrintMemoryUsage();
        DebugBreak();
    }
}

検証結果

ステップ Private Usage Working Set (WSS) PTE状態
MEM_RESERVE 変化なし 変化なし 未割当
MEM_COMMIT +400KB 変化なし not valid (Demand Zero)
データ書き込み 維持 +400KB Valid (PFN割当済)

考察

MEM_COMMIT は単なる「予約の確定」であり、物理メモリ(WSS)を消費しません。CPUがメモリにアクセスし、ページフォールトが発生して初めてOSは物理ページを割り当てます。

また、PAGE_GUARDPAGE_NOACCESS を設定すると、PTEが not valid に書き換わり、そのページは即座にWSSから除外されます。

PAGE_GUARD設定後のログ

前回のPrivate Working Setがこれに対し

Private Working Set:    676 KB

PAGE_GUARD設定すると設定した1ページ分減ります

Private Working Set:    672 KB

これはPAGE_NOACCESSも同様の動きとなります。

この後、1ページずつ再アクセス(Write)すると、再び4KBずつWSSが増加しますが、最初のアクセスでは書き込めないPAGE_GUARDの場合はどうなるか。

PAGE_GUARD への接触時

[SUCCESS] STATUS_GUARD_PAGE_VIOLATION caught!
Working Set Size:  548 KB (+48KB)

ガードページに触れて例外が発生した際、WSSが想定(4KB)よりも大きく(ここでは48KB)増加します。
これは例外ハンドラ(__try/__except)の呼び出しやコンテキストスイッチに伴い、スタックやライブラリのページがWorking Setに引き込まれたためと考えられます。リアルタイム性が求められるシビアな環境では、こうした「付随的なWSS増加」にも注意が必要です。


2. Working Set TrimとTransitionリストの挙動

ではそもそもPAGE_NOACCESSやPAGE_GUARDで解放されたメモリはどこに行くのか。仮説を立てました。
仮説: 「これらは物理メモリの所有権を失って Transition リスト(Standby/Modified)に落ちているはず。だとしたら、これを2GB分作ってメモリを溢れさせたらどうなるのか?」

まずは素直に2GBを確保して0埋めした上で全ページをNOACCESS、PAGE_GUARDで交互に分割してみました。
Standby、Modifiedはそこまで増加しませんでした。

実行前

Standby:   168885 (  675540 kb)
Modified:    16860 (   67440 kb)

実行後

Standby:   136441 (  545764 kb)
Modified:    18514 (   74056 kb)

PTEを確認したところ以下のようになってました。

1: kd> !pte 0x000001C3C79D3000
                                           VA 000001c3c79d3000
PXE at FFFF82C160B05018    PPE at FFFF82C160A03878    PDE at FFFF82C14070F1E0    PTE at FFFF8280E1E3CE98
contains 0A000000577A8867  contains 0A000000C66A9867  contains 0A000000A00C5867  contains 0000000000000280
pfn 577a8     ---DA--UWEV  pfn c66a9     ---DA--UWEV  pfn a00c5     ---DA--UWEV  not valid
                                                                                  DemandZero
                                                                                  Protect: 14 - ReadWrite G

であるならば、属性を変える前に0埋めで書き込んでいるためにメモリ使用量が上がらなかったのかもしれません。では値を入れてみます。

実行前

Standby:   287325 ( 1149300 kb)
Modified:    13829 (   55316 kb)

実行後

Standby:   189080 (  756320 kb)
Modified:   456768 ( 1827072 kb)

Modified が 456,768ページ(約1.8GB)と良い感じに増加。
そしてPTEにTransitionビットが出現しPFNではPFNrestore pte 83F200002284 containing page 0BF5BA Standbyと設定されている。

1: kd> !pte 0x0000026C4D571000
                                           VA 0000026c4d571000
PXE at FFFFCEE773B9D020    PPE at FFFFCEE773A04D88    PDE at FFFFCEE7409B1350    PTE at FFFFCE813626AB88
contains 0A0000011469B867  contains 0A0000011469C867  contains 0A000000BF5BA867  contains 00000000B93C2A80
pfn 11469b    ---DA--UWEV  pfn 11469c    ---DA--UWEV  pfn bf5ba     ---DA--UWEV  not valid
                                                                                  Transition: b93c2
                                                                                  Protect: 14 - ReadWrite G

1: kd> !pfn b93c2
PFN 000B93C2 at address FFFF9D80022BB460
flink 000982C3 blink / share count 000346EB pteaddress FFFFCE813626AB88
reference count 0000 used entry count 0000 Cached color 0 Priority 0
restore pte 83F200002284 containing page 0BF5BA Standby

またPAGE_NOACCESSは以下の通りページファイル行きが予定される事態に。

1: kd> !pte 0x0000026C4D570000
                                           VA 0000026c4d570000
PXE at FFFFCEE773B9D020    PPE at FFFFCEE773A04D88    PDE at FFFFCEE7409B1350    PTE at FFFFCE813626AB80
contains 0A0000011469B867  contains 0A0000011469C867  contains 0A000000BF5BA867  contains 0001113100000302
pfn 11469b    ---DA--UWEV  pfn 11469c    ---DA--UWEV  pfn bf5ba     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 11131
                                                                                  Protect: 18 - No Access

前述のPFNの結果からどうやら検証プロセスが優先度を低くされていることがわかった。(Priority 0)
そのため本プログラムをHIGH_PRIORITY_CLASSに変更。ついでにページファイルに逃がさないようページファイルも無効化。ついでに最強のプログラムであるNotepadに大量の文字を書き込み未保存の状態で実行。
この状態でもNotepadは落ちることなく起動したままとなる。
次はPAGE_NOACCESSとPAGE_GUARDを交互に設定した後、もう一周次はPAGE_GUARDとPAGE_NOACCESSに設定を書き換えてみる。予想としてはフラグを書き換えて終わりだと思うが、念のため。
実行したコードはこちら。

最終形コード(抜粋)

int main() {
    DbgLog("Process ID: %d", GetCurrentProcessId());
    const size_t allocationSize = 4096 * 500000;
    const size_t PAGE_SIZE = 4096;
    const size_t NUM_PAGES = allocationSize / PAGE_SIZE;
    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
    LPVOID p1 = VirtualAlloc(NULL, allocationSize, MEM_RESERVE, PAGE_READONLY);
    if (!p1) return 1;

    LPVOID p2 = VirtualAlloc(p1, allocationSize, MEM_COMMIT, PAGE_READWRITE);

    for (size_t i = 0; i < NUM_PAGES; ++i) {
        ((unsigned char*)p1)[i * PAGE_SIZE] = (unsigned char)(i & 0xFF);
    }

    for (size_t i = 0; i < NUM_PAGES; ++i) {
        // ページごとに属性をバラバラにする
        DWORD old;
        DWORD targetProtect;
        switch (i % 2) {
            case 0: targetProtect = PAGE_NOACCESS; break;
            case 1: targetProtect = PAGE_READWRITE | PAGE_GUARD; break;
        }
        VirtualProtect((LPVOID)((uintptr_t)p1 + (i * PAGE_SIZE)), PAGE_SIZE, targetProtect, &old);

        // 10000ページごとに進捗を出力(出しすぎるとVMが止まるため)
        if (i % 10000 == 0) DbgLog("Allocated %zu pages...\n", i);
    }

    SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
    
    //次は逆にしてもう一周
    for (size_t i = 0; i < NUM_PAGES; ++i) {
        // ページごとに属性をバラバラにする
        DWORD old;
        DWORD targetProtect;
        switch (i % 2) {
            case 0: targetProtect = PAGE_READWRITE | PAGE_GUARD; break;
            case 1: targetProtect = PAGE_NOACCESS; break;
        }
        VirtualProtect((LPVOID)((uintptr_t)p1 + (i * PAGE_SIZE)), PAGE_SIZE, targetProtect, &old);

        // 10000ページごとに進捗を出力(出しすぎるとVMが止まるため)
        if (i % 10000 == 0) DbgLog("Allocated %zu pages...\n", i);
    }

    SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
    
}

そうすると奇妙なことが発生。

実行前

Standby:    67750 (  271000 kb)
Modified:   527406 ( 2109624 kb)

実行後

Standby:    70932 (  283728 kb)
 Modified:   523688 ( 2094752 kb)

明らかに増加量が減少している。原因を調べてみると

0: kd> !pte 0x000001AA1AD60000
                                           VA 000001aa1ad60000
PXE at FFFFA9D4EA753018    PPE at FFFFA9D4EA603540    PDE at FFFFA9D4C06A86B0    PTE at FFFFA980D50D6B00
contains 0A000000A0908867  contains 0A000000A0909867  contains 0A0000005A74B867  contains 0000000000000280
pfn a0908     ---DA--UWEV  pfn a0909     ---DA--UWEV  pfn 5a74b     ---DA--UWEV  not valid
                                                                                  DemandZero
                                                                                  Protect: 14 - ReadWrite G

一部のページがDemandZeroになっている。これはよろしくないと思うので無理やりワーキングセットに引き戻してみた。後から考えれば完全なる逆恨み。
PTEを書き換えて別のpfnと接続し「有効なページ」にする

0: kd> !pte 0x000001AA1AD60000
                                           VA 000001aa1ad60000
PXE at FFFFA9D4EA753018    PPE at FFFFA9D4EA603540    PDE at FFFFA9D4C06A86B0    PTE at FFFFA980D50D6B00
contains 0A000000A0908867  contains 0A000000A0909867  contains 0A0000005A74B867  contains 000000006EB2F867
pfn a0908     ---DA--UWEV  pfn a0909     ---DA--UWEV  pfn 5a74b     ---DA--UWEV  pfn 6eb2f     ---DA--UWEV

0: kd> !pfn 6eb2f
    PFN 0006EB2F at address FFFFD380014C18D0
    flink       000D1430  blink / share count 000C9801  pteaddress FFFFA980D50D6B08
    reference count 0001    used entry count  0000      NonCached color 0   Priority 5
    restore pte 300000300  containing page 05A74B  Free

個人的には正しい状態になったのでそのまま進めてみたところBSOD発生。OSにとっては誤った形という判断。

KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x0000001a
                       (0x0000000000000404,0xFFFFA980D50D6B00,0x000000006EB2F867,0xFFFFA980D50D6B08)

11 00007ff6`893414e2     : 00000000`00000000 000001aa`1ad60000 000001aa`1ab86a80 000001aa`1ab8d540 : KERNELBASE!VirtualFree+0x4a
12 00000000`00000000     : 000001aa`1ad60000 000001aa`1ab86a80 000001aa`1ab8d540 00000000`0007a120 : transitionList_Limit+0x14e2

2章のまとめ:勘違いから見えた「OSの執念」

当初、VirtualProtect で属性を変えれば即座に物理メモリの所有権がリスト(Modified/Standby)へ移ると予想していましたが、実際には 「属性変更(論理)」と「Trim(物理)」は完全に別物 でした。

しかし、あえて値を書き込み、ページファイルを殺し、優先度を上げ、最後にはPTEを力ずくで書き換えてBSODに至るまでの過程で、以下の「Windowsの深層」が浮き彫りになりました。

  1. ゼロページの冷遇: 0で埋められたページは、OSにとって「保存する価値なし」と判断され、即座に再利用リストへ回される。
  2. Transition状態の粘り: 有意なデータを持つページは、プロセスから切り離されてもなお、PTEに Transition ビットを残し、物理メモリの片隅で「飼い主」が戻ってくるのを待っている。
  3. カーネルの鉄槌: PFNとPTEの不整合(PFNデータベースを無視した勝手な接続)を、Windowsカーネルは一瞬で見抜き、システムの整合性を守るために自ら停止(BSOD)を選ぶ。

「なぜBSODまで行ったのか?」——それは、OSという名の管理者が、いかに厳格に物理メモリの整合性を管理しているかを証明するため、避けては通れない道だったのです(ということにしましょう)。
※余談ですが、AIにPFNの書き換えを実施したかどうか判断させたところChatGPTは理論のみ、Geminiは実際に書き換えているとの回答でした。今回必要なところを書き換えていないのでどっちが正しいかは読めば分かります。


おわりに

OSが『保存価値なし』と判断する基準を理解することは、スワップが許されないミッションクリティカルな環境でのメモリ・レジリエンス(回復力)を高める鍵となります。

本当はこの後、共有メモリの検証結果も載せようと思ったのですが、恐らく消化不良になるので前半後半に分けます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?