はじめに
そもそも 12/24 のネタとして考えているのは共有メモリ、それもイメージ ローダーの処理なのですが、前回は CPU によるページングの動作と、NT カーネルがページ テーブルを管理する仕組みについて長々と書いてしまいました。今回は本丸に近づいて共有メモリについて書きたいと思います。カーネルの動作について細かい仕様まで調べだすと永遠に終わらないので、ある程度簡単にまとめます。気が向いたら記事を追記していきます(といって更新しないパターンのやつや・・・)。
PFN データベースとは
いやいや、共有メモリじゃないのかよというツッコミが聞こえてきそうですが、そこに辿り着くまでに調べたことが多いんすよ・・・。さすが OS の要、メモリ マネージャーは複雑。
OS は、何らかの仕組みで物理メモリ上のマップ済み領域や空き領域を把握している必要があります。NT カーネルでは、PFN データベースがその仕組みです。!address を実行して出てくる、PFNDatabase という VaType の領域がそれです。PTE Space と同様 KASLR の対象になるので、アドレスはシステムによって異なります。nt!MmPfnDatabase というシンボルを使って先頭アドレスを取得することができます。
1: kd> !address
BaseAddress EndAddress+1 RegionSize VaType Usage
---------------------------------------------------------------------------------------------------
...snip...
ffffa580`00000000 ffffbf00`00000000 1980`00000000 SystemRange
ffffbf00`00000000 ffffbf00`06200000 0`06200000 PFNDatabase
ffffbf00`06200000 ffffef04`34201000 3004`2e001000 SystemRange
...snip...
1: kd> dq nt!MmPfnDatabase l1
fffff802`56afc500 ffffbf00`00000000
構造は単なる配列で、各 PFN 毎に nt!_MMPFN という構造体を用意し、PFN 0 から順番に並べているだけです。nt!_MMPFN のサイズもデバッガーから分かるので、調べたい PFN に対応する PFN データベースのレコードは簡単に求められます。前回の記事の例を使って、確保した 2MB ページの PFN の情報を見てみました。
1: kd> dt page2mb -b
Local var @ 0x6bef30f898 Type std::unique_ptr<void *,PageFreer>
+0x000 _Mypair : std::_Compressed_pair<PageFreer,void *,1>
+0x000 _Myval2 : 0x00000176`51600000
1: kd> !v2p 0x00000176`51600000
Virtual address = 00000176`51600000
Failed to get msr[c0000080]. Assuming LME is on.
PagingMode: 4-level
DirBase: 00000001`800d0000 ffffa4d2`69349a48
PML4 Table @2 ffffa4d2`69349010 = 0a000001`801ea867 W U A
PageDirPointerTable @473 ffffa4d2`69202ec8 = 0a000001`7fbeb867 W U A
PageDirTable @139 ffffa4d2`405d9458 = 8a000001`820000a5 (2MB Page) XD R U A
PageTable ffffa480`bb28b000 (2MB Page)
Physical Address = 00000001`82000000
1: kd> ?? sizeof(nt!_MMPFN)
unsigned int64 0x30
1: kd> dq ffffbf00`00000000+0x30* 182000 l6
ffffbf00`04860000 00000001`3c397218 ffffa480`bb28b000
ffffbf00`04860010 00000000`00000000 00000000`00000001
ffffbf00`04860020 02000000`00460002 00040010`00000000
1: kd> dt nt!_MMPFN ffffbf00`00000000+0x30*182000
+0x000 ListEntry : _LIST_ENTRY [ 0x00000001`3c397218 - 0xffffa480`bb28b000 ]
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <anonymous-tag>
+0x008 PteAddress : 0xffffa480`bb28b000 _MMPTE
+0x008 PteLong : 0xffffa480`bb28b000
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <anonymous-tag>
+0x024 NodeBlinkLow : 0
+0x026 Unused : 0y0000
+0x026 Unused2 : 0y0000
+0x027 ViewCount : 0x2 ''
+0x027 NodeFlinkLow : 0x2 ''
+0x027 ModifiedListBucketIndex : 0y0010
+0x027 AnchorLargePageSize : 0y10
+0x028 u4 : <anonymous-tag>
PFN が 182000、PFN データベースの開始アドレスが ffffbf00`00000000、nt!_MMPFN のサイズは 0x30 なので、該当する PFN データベース レコードのアドレスは ffffbf00`00000000+0x30*182000 で求められます。すぐに分かるのは、PteAddress というフィールドに入っているアドレスが仮想アドレスに対応する PTE のアドレス 0xffffa480`bb28b000 になっていることです。このように PFN データベースを使うことで、ある物理アドレスが仮想アドレスにマップ済みの場合はその PTE の仮想アドレスを知ることができます。ただし上記の例はラージページなので、前回の記事の最後で少し触れたように PTE は存在せず、仮想アドレスはラージページそのものをマップしています。
ところで、デバッガーには !pfn という PFN データベースのレコードを出力するエクステンションがありますが、全然動きません。謎。レコードの位置を計算式で入力するのは面倒なので、!v2p コマンドの出力の末尾に PFN レコードのサマリを追加したので今後はそれを使います。
pfn - Windows drivers | Microsoft Docs
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-pfn
1: kd> !pfn 182000
c0000094 Exception in kdexts.pfn debugger extension.
PC: 00007ffb`4e769e6b VA: 00000000`00000000 R/W: 0 Parameter: 00000000`00000000
PFN レコードによって、各 PFN の種類が定義されます (ゼロクリア済み、スタンバイ、など)。幾つかステータスについては、同じステータスの PFN レコードをリンクトリストで繋いでそのステータスの PFN をすぐに取得できるようになっています。リストとして繋ぐときには _MMPFN 構造体の u1->NextSlistPfn として定義された _SINGLE_LIST_ENTRY が使われます。したがって u1->NextSlistPfn にアクセス ブレークポイントを置いてデバッグすると、PFN のステータスの変遷がある程度追跡できるはずです。これをライブ デバッグで試してみます。PFN がページから切り離されるときの様子を見たいので、ユーザーモードから NtFreeVirtualMemory が呼ばれたところを捕捉します。第二引数に解放される仮想アドレスが入っています。
NtFreeVirtualMemory function (ntifs.h) - Windows drivers | Microsoft Docs
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntfreevirtualmemory
0: kd> bl
0 d 00007ffd`a29cc150 e 1 0001 (0001) ntdll!NtFreeVirtualMemory
0: kd> be0
0: kd> g
Breakpoint 0 hit
ntdll!NtFreeVirtualMemory:
0033:00007ffd`a29cc150 4c8bd1 mov r10,rcx
1: kd> !process -1 0
PROCESS ffff850f2ae4e280
SessionId: 0 Cid: 06cc Peb: e07d737000 ParentCid: 02cc
DirBase: 1b7d5d000 ObjectTable: ffff98824ea51580 HandleCount: 145.
Image: svchost.exe
1: kd> knL
# Child-SP RetAddr Call Site
00 000000e0`7d97dbf8 00007ffd`a2940e88 ntdll!NtFreeVirtualMemory
01 000000e0`7d97dc00 00007ffd`a2943e18 ntdll!RtlpHpFreeVA+0x5c
02 000000e0`7d97dc40 00007ffd`a2944197 ntdll!RtlpHpSegMgrCommit+0x178
03 000000e0`7d97dd00 00007ffd`a294ffdd ntdll!RtlpHpSegPageRangeCommit+0x207
04 000000e0`7d97dda0 00007ffd`a294f6a3 ntdll!RtlpHpSegPageRangeCoalesce+0x28d
05 000000e0`7d97de10 00007ffd`a2946ac1 ntdll!RtlpHpSegFree+0x153
06 000000e0`7d97de80 00007ffd`a2949321 ntdll!RtlpHpLfhSubsegmentFreeBlock+0x571
07 000000e0`7d97df10 00007ffd`a2945e14 ntdll!RtlpFreeHeapInternal+0x1c1
08 000000e0`7d97dfd0 00007ffd`a2945d3d ntdll!RtlpHpFreeWithExceptionProtection+0x24
09 000000e0`7d97e040 00007ffd`a09b8cd5 ntdll!RtlFreeHeap+0x6d
0a 000000e0`7d97e080 00007ffd`a27de619 combase!ActivationPropertiesIn::Release+0x85
0b 000000e0`7d97e0b0 00007ffd`a27dcd86 RPCRT4!NdrpFreeParams+0x269
0c 000000e0`7d97e110 00000000`00000000 RPCRT4!NdrStubCall2+0xc46
1: kd> !v2p poi(@rdx)
Virtual address = 0000021b`f7060000
PagingMode: 4-level
DirBase: 00000001`b7d5d000 ffffe371`b8dc6e30
PML4 Table @4 ffffe371`b8dc6020 = 0a000001`b7c7c867 W U A
PageDirPointerTable @111 ffffe371`b8c04378 = 0a000001`b7c7d867 W U A
PageDirTable @440 ffffe371`8086fdc0 = 0a000001`b7fa3867 W U A
PageTable @96 ffffe301`0dfb8300 = 80000001`fe76e867 XD W U A
Physical Address = 00000001`fe76e000
PFN@1fe76e ffffdf80`05fb64a0: ffffe301`0dfb8300 {00000080} #1b7fa3 0 0 0 Exist
1: kd> dq ffffdf80`05fb64a0 l6
ffffdf80`05fb64a0 00000000`00000001 ffffe301`0dfb8300
ffffdf80`05fb64b0 00000000`00000080 00000000`00000001
ffffdf80`05fb64c0 00000000`05560001 00040000`001b7fa3
RPCRT4 が FreeHeap 経由で仮想アドレス 0000021b`f7060000 を解放しようとしています。ffffdf80`05fb64a0 が PFN レコードの先頭で、リンクトリストで繋がれるはずのアドレスなので、ここにアクセス ブレークポイントをセットします。
1: kd> ba w8 ffffdf80`05fb64a0
1: kd> bl
0 e 00007ffd`a29cc150 e 1 0001 (0001) ntdll!NtFreeVirtualMemory
1 e ffffdf80`05fb64a0 w 8 0001 (0001)
1: kd> bd0
1: kd> g
Breakpoint 1 hit
nt!MiInsertPageInFreeOrZeroedList+0x8aa:
fffff806`73f4750a 4c896f10 mov qword ptr [rdi+10h],r13
1: kd> dq ffffdf80`05fb64a0 l6
ffffdf80`05fb64a0 0000000f`ffffffff ffffe301`0dfb8300
ffffdf80`05fb64b0 00000000`00000080 8c00000f`ffffffff
ffffdf80`05fb64c0 00000000`00410000 00040000`001b7fa3
1: kd> knL
# Child-SP RetAddr Call Site
00 ffff8206`db78ea80 fffff806`73f43ab7 nt!MiInsertPageInFreeOrZeroedList+0x8aa
01 ffff8206`db78eba0 fffff806`73efba7e nt!MiDeletePteList+0x547
02 ffff8206`db78ec80 fffff806`742fb97d nt!MiDecommitPages+0x121e
03 ffff8206`db78f850 fffff806`742fafe3 nt!MiDecommitRegion+0x7d
04 ffff8206`db78f8d0 fffff806`742fa8d5 nt!MmFreeVirtualMemory+0x6d3
05 ffff8206`db78fa20 fffff806`74006bb5 nt!NtFreeVirtualMemory+0x95
06 ffff8206`db78fa80 00007ffd`a29cc164 nt!KiSystemServiceCopyEnd+0x25
07 000000e0`7d97dbf8 00007ffd`a2940e88 ntdll!NtFreeVirtualMemory+0x14
08 000000e0`7d97dc00 00007ffd`a2943e18 ntdll!RtlpHpFreeVA+0x5c
09 000000e0`7d97dc40 00007ffd`a2944197 ntdll!RtlpHpSegMgrCommit+0x178
0a 000000e0`7d97dd00 00007ffd`a294ffdd ntdll!RtlpHpSegPageRangeCommit+0x207
0b 000000e0`7d97dda0 00007ffd`a294f6a3 ntdll!RtlpHpSegPageRangeCoalesce+0x28d
0c 000000e0`7d97de10 00007ffd`a2946ac1 ntdll!RtlpHpSegFree+0x153
0d 000000e0`7d97de80 00007ffd`a2949321 ntdll!RtlpHpLfhSubsegmentFreeBlock+0x571
0e 000000e0`7d97df10 00007ffd`a2945e14 ntdll!RtlpFreeHeapInternal+0x1c1
0f 000000e0`7d97dfd0 00007ffd`a2945d3d ntdll!RtlpHpFreeWithExceptionProtection+0x24
10 000000e0`7d97e040 00007ffd`a09b8cd5 ntdll!RtlFreeHeap+0x6d
11 000000e0`7d97e080 00007ffd`a27de619 combase!ActivationPropertiesIn::Release+0x85
12 000000e0`7d97e0b0 00007ffd`a27dcd86 RPCRT4!NdrpFreeParams+0x269
13 000000e0`7d97e110 00000000`00000000 RPCRT4!NdrStubCall2+0xc46
1: kd> g
同じスレッドの中でメモリマネージャーが nt!MiInsertPageInFreeOrZeroedList を呼び出して、u1->NextSlistPfn に 0000000f`ffffffff という値がセットされました。とりあえず続行します。
Breakpoint 1 hit
nt!MiUnlinkFreeOrZeroedPage+0x72d:
fffff806`73f4883d 4d85c0 test r8,r8
1: kd> !process -1 0
PROCESS ffff850f25c85040
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001aa000 ObjectTable: ffff98824ae64f00 HandleCount: 2294.
Image: System
1: kd> knL
# Child-SP RetAddr Call Site
00 ffff8206`d98aa7b0 fffff806`73f45c4d nt!MiUnlinkFreeOrZeroedPage+0x72d
01 ffff8206`d98aa840 fffff806`73f44f78 nt!MiZeroPage+0xbed
02 ffff8206`d98aa9f0 fffff806`73fb15d8 nt!MiZeroLargePages+0xd08
03 ffff8206`d98aaae0 fffff806`73ea29a5 nt!MiZeroLargePageThread+0x88
04 ffff8206`d98aab90 fffff806`73ffc868 nt!PspSystemThreadStartup+0x55
05 ffff8206`d98aabe0 00000000`00000000 nt!KiStartSystemThread+0x28
1: kd> dq ffffdf80`05fb64a0 l6
ffffdf80`05fb64a0 00000000`00000000 ffffe301`0dfb8300
ffffdf80`05fb64b0 00000000`00000080 8c000000`00000000
ffffdf80`05fb64c0 00000000`00450000 00040000`001b7fa3
1: kd> g
Breakpoint 1 hit
nt!ExpInterlockedPushEntrySList+0x1e:
fffff806`73ffd18e 488d9801000100 lea rbx,[rax+10001h]
1: kd> dq ffffdf80`05fb64a0 l6
ffffdf80`05fb64a0 ffffdf80`04ea94a0 ffffe301`0dfb8300
ffffdf80`05fb64b0 fffffffe`00000080 8c000000`00000000
ffffdf80`05fb64c0 00000000`00450000 00040000`001b7fa3
1: kd> knL
# Child-SP RetAddr Call Site
00 ffff8206`d98aa710 fffff806`73f472f1 nt!ExpInterlockedPushEntrySList+0x1e
01 ffff8206`d98aa720 fffff806`73f45c57 nt!MiInsertPageInFreeOrZeroedList+0x691
02 ffff8206`d98aa840 fffff806`73f44f78 nt!MiZeroPage+0xbf7
03 ffff8206`d98aa9f0 fffff806`73fb15d8 nt!MiZeroLargePages+0xd08
04 ffff8206`d98aaae0 fffff806`73ea29a5 nt!MiZeroLargePageThread+0x88
05 ffff8206`d98aab90 fffff806`73ffc868 nt!PspSystemThreadStartup+0x55
06 ffff8206`d98aabe0 00000000`00000000 nt!KiStartSystemThread+0x28
1: kd> dl ffffdf80`05fb64a0 100 2
ffffdf80`05fb64a0 ffffdf80`04ea94a0 ffffe301`0dfb8300
ffffdf80`04ea94a0 ffffdf80`04db34a0 ffffe300`e3486330
ffffdf80`04db34a0 ffffdf80`05ea84a0 ffffe300`e3488d80
ffffdf80`05ea84a0 ffffdf80`05ede4a0 ffff9882`506d4000
ffffdf80`05ede4a0 ffffdf80`05e664a0 80000000`00000000
ffffdf80`05e664a0 ffffdf80`05eba4a0 80000000`00000000
ffffdf80`05eba4a0 ffffdf80`05e8d4a0 80000000`00000000
ffffdf80`05e8d4a0 ffffdf80`04e3a4a0 80000000`00000000
ffffdf80`04e3a4a0 ffffdf80`050bc4a0 ffffe301`14b6c0a0
ffffdf80`050bc4a0 ffffdf80`051a94a0 ffffe300`e37b0b20
ffffdf80`051a94a0 ffffdf80`05b754a0 ffffe33f`fed154f0
ffffdf80`05b754a0 ffffdf80`04d264a0 ffffe33f`fed15588
ffffdf80`04d264a0 ffffdf80`04ea64a0 ffffe300`eb62b8d8
ffffdf80`04ea64a0 ffffdf80`0496f4a0 ffffe33f`fecf6a18
ffffdf80`0496f4a0 00000000`00000000 ffffe371`9ffdf3f0
今度はカーネル プロセスの nt!MiZeroPage が nt!MiInsertPageInFreeOrZeroedList を呼び出して、u1->NextSlistPfn に 0 を一旦セットしてから、再度別の PFN レコードのアドレスである ffffdf80`04ea94a0 をセットしました。どうやらリンクトリストに追加されたようです。nt!MiInsertPageInFreeOrZeroedList や nt!MiZeroPage という関数の名前から判断すると、最初に 0x0000000f`ffffffff をセットする処理は PFN を解放済みページとしてゼロクリア処理の対象としてマークする処理ではないかと考えられます。
nt!MiZeroPage を呼ぶカーネル スレッドは、インサイド Windows 第 4 版ではゼロページスレッドと呼ばれているスレッドで、解放済みページをゼロで埋めるだけの簡単なお仕事をしています。VirtualAlloc で確保されたバッファーは 0 クリアされていることが保証されていますが、その理由がこの仕組みです。このスレッドは優先度レベル 0 で動作するため、CPU の空き時間にのみ実行されるらしいです。
ゼロページスレッドが、どのようにゼロクリアする対象のページを見つけているのかまでは調べていません。インサイド Windows には、ゼロページスレッドはフリーページリストというリストを見ていることが書かれているので、ゼロページリストとは別の方法でリストを持っているのかもしれません。nt!MiZeroLargePages から細かくデバッグしていけば分かりそうですが、次のトピックに移りたいので、ここまでにしておきます。
以上、PFN データベースを使うと PFN に対するステータスを知ることができ、逆に、特定のステータスを持つ PFN を探すこともリストを辿ることで可能になっていることが分かりました。
Virtual Address Descriptor (= VAD)
次に VAD です。後の説明で必要になるので、先に書いておきます。
VAD とは、仮想アドレスの範囲についてのメタ情報を保存する構造体で、nt!_MMVAD として定義されています。仮想アドレスの範囲は、前回の記事でも出てきた VPN (= Virtual Page Number) を使って、以下の StartingVpn[High] と EndingVpn で指定されます。つまり、必ず 4KB ページ単位になります。
nt!_MMVAD
+0x000 Core : _MMVAD_SHORT
+0x000 NextVad : Ptr64 _MMVAD_SHORT
+0x008 ExtraCreateInfo : Ptr64 Void
+0x000 VadNode : _RTL_BALANCED_NODE
+0x018 StartingVpn : Uint4B
+0x01c EndingVpn : Uint4B
+0x020 StartingVpnHigh : UChar
+0x021 EndingVpnHigh : UChar
+0x022 CommitChargeHigh : UChar
+0x023 SpareNT64VadUChar : UChar
+0x024 ReferenceCount : Int4B
+0x028 PushLock : _EX_PUSH_LOCK
+0x030 u : <anonymous-tag>
+0x034 u1 : <anonymous-tag>
+0x038 EventList : Ptr64 _MI_VAD_EVENT_BLOCK
+0x040 u2 : <anonymous-tag>
+0x048 Subsection : Ptr64 _SUBSECTION
+0x050 FirstPrototypePte : Ptr64 _MMPTE
+0x058 LastContiguousPte : Ptr64 _MMPTE
+0x060 ViewLinks : _LIST_ENTRY
+0x070 VadsProcess : Ptr64 _EPROCESS
+0x078 u4 : <anonymous-tag>
+0x080 FileObject : Ptr64 _FILE_OBJECT
各プロセスは、予約済み、もしくはコミット済みの仮想アドレスの範囲についての VAD を AVL ツリーとして保持していて、これを VAD ツリーと呼びます。いわば仮想アドレス版の PFN データベースみたいなものです。ツリーは開始 VPN をもとにソートされており、与えられたアドレスに対する VAD を効率的に見つけることができます。
ツリーのルートが nt!_EPROCESS::VadRoot に保存されているので、プロセスの保持する VAD ツリーをデバッガーで見つけるのは簡単です。!vad というコマンドで VAD ツリーを全部ダンプすることができます。ユーザーモード デバッガーの !address の出力に似ています。
vad - Windows drivers | Microsoft Docs
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-vad
lkd> !process 0 0 notepad.exe
PROCESS ffff810cf4e18080
SessionId: 1 Cid: 36c4 Peb: 850026c000 ParentCid: 2710
DirBase: 5ba3cd000 ObjectTable: ffff9a8ebb1f3880 HandleCount: 270.
Image: notepad.exe
lkd> !process ffff810cf4e18080 1
PROCESS ffff810cf4e18080
SessionId: 1 Cid: 36c4 Peb: 850026c000 ParentCid: 2710
DirBase: 5ba3cd000 ObjectTable: ffff9a8ebb1f3880 HandleCount: 270.
Image: notepad.exe
VadRoot ffff810cf981be40 Vads 119 Clone 0 Private 795. Modified 30. Locked 0.
DeviceMap ffff9a8ebce114e0
...snip...
lkd> !vad ffff810cf981be40
VAD Level Start End Commit
ffff810cfbe75990 5 17490 17490 1 Private READWRITE
ffff810cfbe75b20 4 174a0 174a0 1 Private READWRITE
ffff810cfbe6d150 5 7ffe0 7ffe0 1 Private READONLY
ffff810cfbe6e7d0 3 7ffe6 7ffe6 1 Private READONLY
ffff810cfbe72790 4 8500100 850017f 20 Private READWRITE
ffff810cfbe71700 5 8500180 85001ff 20 Private READWRITE
ffff810cfbe6eb90 2 8500200 85003ff 15 Private READWRITE
...snip...
ffff810cf9802620 5 7ffb989b0 7ffb99096 15 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\shell32.dll
ffff810cf8bbff00 7 7ffb990a0 7ffb991f6 6 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ole32.dll
ffff810cf981b120 6 7ffb99240 7ffb9942f 17 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
Total VADs: 119, average level: 6, maximum depth: 7
Total private commit: 0x379 pages (3556 KB)
Total shared commit: 0x1b2f pages (27836 KB)
適当な VAD のアドレスに対して !vad の第二引数として 1 を渡して実行すると、より詳細な情報を出力できます。
lkd> !vad ffff810cf981b120 1
VAD @ ffff810cf981b120
Start VPN 7ffb99240 End VPN 7ffb9942f Control Area ffff810ceb049c90
FirstProtoPte ffff9a8eb4a63010 LastPte ffff9a8eb4a63f88 Commit Charge 11 (0n17)
Secured.Flink 0 Blink 0 Banked/Extend 0
File Offset 0
ImageMap ViewShare EXECUTE_WRITECOPY
ControlArea @ ffff810ceb049c90
Segment ffff9a8eb4f45030 Flink ffff810cf981aa00 Blink ffff810ce22e4b60
Section Ref 2 Pfn Ref 177 Mapped Views b6
User Ref b8 WaitForDel 0 Flush Count 9f40
File Object ffff810ceb2fd1e0 ModWriteCount 0 System Views 66bc
WritableRefs 48001f PartitionId 0
Flags (a0) Image File
\Windows\System32\ntdll.dll
Segment @ ffff9a8eb4f45030
ControlArea ffff810ceb049c90 BasedAddress 00007ffb99240000
Total Ptes 1f0
Segment Size 1f0000 Committed 0
Image Commit 10 Image Info ffff9a8eb4f45078
ProtoPtes ffff9a8eb4a63010
Flags (c4822000) DebugSymbolsLoaded ProtectionMask
Reload command: .reload ntdll.dll=00007ffb`99240000,1f0000
VAD については以上です。
PTE の種類について
まだ出てこない共有メモリ・・・あと一息です。
次は PTE について。PTE なんて前の記事で散々やっただろう、という気もしますが、ここでは CPU によるページングではなく OS の視点から考えます。これも後で出てくるんすよ・・・。
CPU からすると PTE は 1 種類で、ビットをどう切るかという話だけでした。もし PTE の P フラグが 0 だった場合、それは単に無効な PTE としてページフォルトを発生させるだけの存在となり、他のビットは無視します。このことを利用して、OS は無効な PTE の残りのビットを自由に使うことができます。Windows では、P フラグが 1 で CPU がページングに利用するページをハードウェア PTE と呼び、その他の OS が利用する PTE をソフトウェア PTE と呼んでいます。
NT カーネルでは、PTE は nt!_MMPTE という構造体で定義しています。_MMPTE_HARDWARE がハードウェア PTE で、CPU がページングで使うビットをビットフィールドとして定義しています。残りがの構造体がソフトウェア PTE で、以下のように複数種類の構造を共用体としてまとめてあります。
1: kd> dt nt!_MMPTE u.
+0x000 u :
+0x000 Long : Uint8B
+0x000 VolatileLong : Uint8B
+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
共有メモリを扱う上で重要な構造が _MMPTE_PROTOTYPE です。P フラグが 0 で、かつ u.Proto.Prototype ビット (1 << 0n10 のビット) が 1 であるときの PTE です。
その他、よく出てくる PTE の判別方法が以下のページにフローチャート付きで紹介されており、分かりやすいです。
Windows Memory Introspection with IceBox
https://thalium.github.io/blog/posts/windows-full-memory-introspection-with-icebox/
ところで、プロトタイプ PTE という言葉の定義には注意が必要です。上の説明ではあえてプロトタイプ PTE という言葉を使わず、_MMPTE_PROTOTYPE 構造体と書きました。構造体の名前を考えると、プロトタイプ PTE とは、Prototype ビットが ON の PTE のことであると考えるのが自然です。しかし、これから説明するようにデータ構造を総合的に考えると、プロトタイプ PTE は Prototype ビットの値ではなく、その役割で定義される方が分かりやすくなります。例えば、Prototype フラグが 1 だったとしても、PTE Space にあればそれはただの PTE であると考えた方が共有メモリについて矛盾がなく理解できるはずです。後でプロトタイプ PTE が出てきたときにまた触れたいと思います。
これは完全に余談ですが、ソフトウェア PTE のようにハードウェア側が想定していない手法をソフトウェア側が勝手に利用しているというエピソードとして、昔の Windows が無効 CPU 命令に対する割り込みを多用していたという話を思い出しました。好きな話です。
The hunt for a faster syscall trap | The Old New Thing
https://devblogs.microsoft.com/oldnewthing/20041215-00/?p=37003
共有メモリ
さて、満を持して共有メモリの話です。このシリーズを忍耐強くここまで読んでくださっている方には今更だと思いますが、共有メモリとは、プロセス間で共有されるメモリのことを意味する汎用的な単語で、Windows ではセクション オブジェクト (ユーザーモードではファイル マッピング オブジェクト) を使うことで実現できます。ユーザー モードとカーネル モードで名前が違う理由はよく分かりません。NT 以前の OS との互換性じゃないかと予想してますが。そのうち Raymond Chen 氏がネタにしてくれないだろうか。
共有メモリは、その性質上、通常のプライベートメモリとは異なる管理のされ方をします。とりあえずページングの部分を見てみます。前回の記事で使ったリポジトリをビルドしてできる g.exe を使います。
msmania/mmdemo: NT Kernel Memory Manager demo
https://github.com/msmania/mmdemo/
起動すると、ボタンを 2 つ持つウィンドウが開くだけですが、実はプロセスが 2 つ作られていて、一つのセクション オブジェクトをともにマップしています。ちなみに外観はこんな感じです。
ウィンドウを表示しているのは子プロセスで、Touch ボタンをクリックすると子プロセスが共有メモリに値を書き込みます。Forget ボタンをクリックすると、SetProcessWorkingSetSize を使ってワーキングセットを最大限減らす処理を行います。
SetProcessWorkingSetSize function (winbase.h) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setprocessworkingsetsize
デバッグを簡単にするため、両プロセスではセクション オブジェクトがマップされた仮想アドレスをグローバル変数の gView に保存しているので、その値をカーネル デバッガーで取得してページングの様子を確認します。
1: kd> !process 0 0 g.exe
PROCESS ffffb908964020c0
SessionId: 1 Cid: 0bcc Peb: f5d0b4a000 ParentCid: 1760
DirBase: 156f8e000 ObjectTable: ffffd404e5345cc0 HandleCount: 48.
Image: g.exe
PROCESS ffffb90898868080
SessionId: 1 Cid: 108c Peb: c98059f000 ParentCid: 0bcc
DirBase: 174c8a000 ObjectTable: ffffd404e5347980 HandleCount: 145.
Image: g.exe
1: kd> .process /i ffffb908964020c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
1: kd> .reload
1: kd> x g!gView
00007ff6`f1e36218 g!gView = 0x00000242`43020000
1: kd> !v2p 0x00000242`43020000
Virtual address = 00000242`43020000
PagingMode: 4-level
DirBase: 00000001`56f8e000 ffff8040`20100800
PML4 Table @4 ffff8040`20100020 = 0a000001`83ba9867 W U A
PageDirPointerTable @265 ffff8040`20004848 = 0a000001`835aa867 W U A
PageDirTable @24 ffff8040`009090c0 = 0a000001`743b2867 W U A
PageTable @32 ffff8001`21218100 = 91000001`7ed3e005 XD R U
Physical Address = 00000001`7ed3e000
PFN@17ed3e ffffcf00`047c7ba0: ffffd404`e3cf7210 {00000080} #17b2fa 0 0 0 Exist Proto
1: kd> .process /i ffffb90898868080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
1: kd> x g!gView
00007ff6`f1e36218 g!gView = 0x000001f7`0e9e0000
1: kd> !v2p 0x000001f7`0e9e0000
Virtual address = 000001f7`0e9e0000
PagingMode: 4-level
DirBase: 00000001`74c8a000 ffff8040`20100800
PML4 Table @3 ffff8040`20100018 = 0a000001`5cfa5867 W U A
PageDirPointerTable @476 ffff8040`20003ee0 = 0a000000`00aa6867 W U A
PageDirTable @116 ffff8040`007dc3a0 = 0a000001`8366e867 W U A
PageTable @480 ffff8000`fb874f00 = c1000001`7ed3e847 XD W U
Physical Address = 00000001`7ed3e000
PFN@17ed3e ffffcf00`047c7ba0: ffffd404`e3cf7210 {00000080} #17b2fa 0 0 0 Exist Proto
2 つのプロセスの異なる仮想アドレス (0x00000242`43020000 と 0x000001f7`0e9e0000) がともに物理アドレス 00000001`7ed3e000 をマップしています。ちゃんと共有されています。
共有メモリがプライベート メモリと異なる点の一つが、PFN レコードです。プライベート メモリでは、PFN データベースのところで見たように _MMPFN::PteAddress のフィールドが PTE のアドレスを示していました、しかし上記出力にある ffffd404`e3cf7210 というアドレスは、親プロセス、子プロセスいずれの PTE でもありません。また、他の PTE のアドレスと 47:39 の場所のビットの値が異なるため、このアドレスは PTE Space に属していないことが分かります。これがプロトタイプ PTE です。
!v2p の出力の最後の行に Proto というラベルがついています。これは PFN データベース側で _MMPFN::u4.PrototypePte のビットが ON になっていることを示しています。次にプロトタイプ PTE の中身も見てみましょう。
1: kd> dq ffffd404`e3cf7210 l1
ffffd404`e3cf7210 8a000001`7ed3e921
1: kd> dt nt!_MMPTE ffffd404`e3cf7210 u.Hard.
+0x000 u :
+0x000 Hard :
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y0
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000101111110110100111110 (0x17ed3e)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y1010
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1
PFN レコード側で PrototypePte ビットが ON になっていましたが、この PTE は Valid フラグが ON なので、構造は _MMPTE_PROTOTYPE ではなく _MMPTE_HARDWARE です。ハードウェア PTE として見ると PFN は 0x17ed3e を示していて、既に確認した物理アドレス 00000001`7ed3e000 と矛盾なく一致します。
前の章でプロトタイプ PTE の定義について述べました。もし Prototype フラグをベースに定義を決めてしまうと、今回のケースにプロトタイプ PTE は存在していないことになります。しかしこの例では、PFN レコードのフラグに沿って ffffd404`e3cf7210 をプロトタイプ PTE と考えた方がすっきりします。
定義の問題はさておき、現在の状態は 3 つの PTE が一つの PFN を指し示していることになります。この様子を図示しました。
この状態で、[Forget] ボタンをクリックします。クリックしても見た目には何の変化もありませんが、子プロセスのワーキングセットからこの共有メモリのページが削除されるはずです。デバッガーで止めて、子プロセスの仮想アドレスのページングをもう一度確認します。
1: kd> .process /i ffffb90898868080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
1: kd> !v2p 000001f7`0e9e0000
Virtual address = 000001f7`0e9e0000
PagingMode: 4-level
DirBase: 00000001`74c8a000 ffff8040`20100800
PML4 Table @3 ffff8040`20100018 = 0a000001`5cfa5867 W U A
PageDirPointerTable @476 ffff8040`20003ee0 = 0a000000`00aa6867 W U A
PageDirTable @116 ffff8040`007dc3a0 = 0a000001`8366e867 W U A
PageTable @480 ffff8000`fb874f00 = ffffffff`00000480 (inactive)
PTE の P フラグが 0 になり、共有メモリがワーキングセットから削除されたことが分かります。したがって、ffff8000`fb874f00 をソフト PTE として扱うことができます。今までの流れから予想できるかもしれませんが、この PTE は Prototype フラグが 1 になっており、_MMPTE_PROTOTYPE 構造体になっています。
1: kd> dt nt!_MMPTE ffff8000`fb874f00 u.Proto.
+0x000 u :
+0x000 Proto :
+0x000 Valid : 0y0
+0x000 DemandFillProto : 0y0
+0x000 HiberVerifyConverted : 0y0
+0x000 ReadOnly : 0y0
+0x000 SwizzleBit : 0y0
+0x000 Protection : 0y00100 (0x4)
+0x000 Prototype : 0y1
+0x000 Combined : 0y0
+0x000 Unused1 : 0y0000
+0x000 ProtoAddress : 0y111111111111111111111111111111110000000000000000 (0xffffffff0000)
さらに ProtoAddress=0xffffffff0000 なので、前の章で紹介した thalium.github.io のページによると、具体的な PFN の代わりに VAD に誘導する PTE ということになります。実はこのことは !pte コマンドでも確かめられます。PTE のところに "Proto: VAD" と表示されます。
1: kd> !pte 000001f7`0e9e0000
VA 000001f70e9e0000
PXE at FFFF804020100018 PPE at FFFF804020003EE0 PDE at FFFF8040007DC3A0 PTE at FFFF8000FB874F00
contains 0A0000015CFA5867 contains 0A00000000AA6867 contains 0A0000018366E867 contains FFFFFFFF00000480
pfn 15cfa5 ---DA--UWEV pfn aa6 ---DA--UWEV pfn 18366e ---DA--UWEV not valid
Proto: VAD
Protect: 4 - ReadWrite
この状態で、子プロセスが仮想アドレス 000001f7`0e9e0000 にアクセスした場合、PTE が無効であるため CPU はページ フォルトを発生させます。ページ フォルト ハンドラーは、フォルトを発生させたスレッドのコンテキストのまま実行され、そのとき CR2 レジスターにはページフォルトを起こしたアドレスが入ります。したがってメモリ マネージャーは、CR2 から該当の PTE が VAD Prototype PTE であること判断し、VAD ツリーを探索して、その仮想アドレスが所属している VAD を取得することができます。
実際に子プロセスの VAD ツリーから、000001d1`c4c10000 を管理する VAD を取得してみましょう。
1: kd> !process -1 1
PROCESS ffffb90898868080
SessionId: 1 Cid: 108c Peb: c98059f000 ParentCid: 0bcc
DirBase: 174c8a000 ObjectTable: ffffd404e5347980 HandleCount: 144.
Image: g.exe
VadRoot ffffb908998a8840 Vads 67 Clone 0 Private 272. Modified 297. Locked 0.
...snip...
1: kd> !vad ffffb908998a8840
VAD Level Start End Commit
ffffb908998a7c60 5 7ffe0 7ffe0 1 Private READONLY
...snip...
ffffb908998a84d0 1 1f70e9d0 1f70e9d0 1 Private READWRITE
ffffb908936e3a90 6 1f70e9e0 1f70e9e0 0 Mapped READWRITE Pagefile section, shared commit 0x1
ffffb908998a86b0 5 1f70e9f0 1f70e9fc 1 Private READWRITE
...
1: kd> !vad ffffb908936e3a90 1
VAD @ ffffb908936e3a90
Start VPN 1f70e9e0 End VPN 1f70e9e0 Control Area ffffb908966a5c90
FirstProtoPte ffffd404e3cf7210 LastPte ffffd404e3cf7210 Commit Charge 0 (0n0)
Secured.Flink 0 Blink 0 Banked/Extend 0
File Offset 0
ViewShare READWRITE
ControlArea @ ffffb908966a5c90
Segment ffffd404e4f11050 Flink ffffb908936e3af0 Blink ffffb90898d1d160
Section Ref 1 Pfn Ref 0 Mapped Views 2
User Ref 3 WaitForDel 0 Flush Count 1
File Object 0000000000000000 ModWriteCount 0 System Views 0
WritableRefs 0 PartitionId 0
Flags (2000) Commit
Pagefile-backed section
Segment @ ffffd404e4f11050
ControlArea ffffb908966a5c90 ExtendInfo 0000000000000000
Total Ptes 1
Segment Size 1000 Committed 1
CreatingProcessId bcc FirstMappedVa 24243020000
ProtoPtes ffffd404e3cf7210
Flags (80000) ProtectionMask
VAD は ffffb908936e3a90 で、対応する仮想アドレスがページ ファイルによる共有メモリである、という情報が保持されていると分かります。これは nt!_MMVAD::Subsection から _CONTROL_AREA 構造体を参照できるからです。メモリ マネージャーも同様に、フォルトを発生させたアドレスが共有メモリであると知ることができます。さらに重要なのは、"ProtoPtes ffffd404e3cf7210" という情報です。この ffffd404e3cf7210 は PFN レコードが指し示していたプロトタイプ PTE のアドレスで、_MMPTE_HARDWARE として PFN 17ed3e を示していました。したがってメモリ マネージャーは、PTE → VAD → プロトタイプ PTE と参照することで、マップすべきデータは既に物理メモリ上にロード済みであることが分かり、ページファイルにアクセスする面倒な手順を飛ばして子プロセスの PTE を変更するだけでいい、と判断できます。いわゆるソフト ページフォルトというやつです。
メモリ マネージャーが辿るべきであろうデータの繋がりを以下の図に示しました。ここで重要なのは、CPU 的には無効であるはずの PTE Space 内の Proto VAD が、実は VAD と Subsection を経由して Prototype PTE にリンクして、そこからマップすべき PFN を取得できる点です。Proto VAD は特定の VAD を指しているわけではなく、VAD ツリーの探索は必要なので破線の矢印で示しています。
実行可能モジュールの共有メモリ
本記事最後のトピックです。前章で扱った g.exe の共有メモリは、CreateFileMapping の第一引数にINVALID_HANDLE_VALUE を渡して作成した、ページング ファイル ベースの共有メモリ、というか、特定のファイルをベースとしない共有メモリでした。このへんの混乱については Raymond Chen 氏が書いてくれています。
The source of much confusion: "backed by the system paging file" | The Old New Thing
https://devblogs.microsoft.com/oldnewthing/20130301-00/?p=5093
しかし、わざわざ CreateFileMapping を使わなくても、共有メモリが必ず使われる場所が身近にあります。それが EXE や DLL などの実行可能ファイルをロードするときです。実行可能ファイルは、大部分のデータが動的に変更されないことが予想されるため、プロセス毎にプライベート メモリとして確保するのは物理メモリの無駄になります。したがって Copy-on-write 属性で共有メモリとしてマップされます。では g.exe がロードされたメモリ アドレスを使って先ほどと同様のデバッグを行ないます。
1: kd> !process 0 0 g.exe
PROCESS ffffb908964020c0
SessionId: 1 Cid: 0938 Peb: 37cb7d1000 ParentCid: 1760
DirBase: 1f84c8000 ObjectTable: ffffd404e4b9f7c0 HandleCount: 48.
Image: g.exe
PROCESS ffffb90896264300
SessionId: 1 Cid: 16e8 Peb: bdd0a59000 ParentCid: 0938
DirBase: 1f29c0000 ObjectTable: ffffd404e4b9f280 HandleCount: 144.
Image: g.exe
1: kd> .process /i ffffb908964020c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
0: kd> lm m g
start end module name
00007ff6`f1db0000 00007ff6`f1e44000 g C (private pdb symbols) d:\symbols\g.pdb\4BDD3528A7F34C978DBA811B234C61ED4\g.pdb
0: kd> !v2p 00007ff6`f1db0000
Virtual address = 00007ff6`f1db0000
PagingMode: 4-level
DirBase: 00000001`f84c8000 ffff8040`20100800
PML4 Table @255 ffff8040`201007f8 = 0a000001`f90d4867 W U A
PageDirPointerTable @475 ffff8040`200ffed8 = 0a000001`76bd5867 W U A
PageDirTable @398 ffff8040`1ffdbc70 = 0a000001`752d6867 W U A
PageTable @432 ffff803f`fb78ed80 = 80000001`40657025 XD R U A
Physical Address = 00000001`40657000
PFN@140657 ffffcf00`03c13050: ffffd404`e1b37a90 {b908974b`e2f00420} #14a1f1 0 0 0 Exist Proto
0: kd> .process /i ffffb90896264300
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
0: kd> lm m g
start end module name
00007ff6`f1db0000 00007ff6`f1e44000 g C (private pdb symbols) d:\symbols\g.pdb\4BDD3528A7F34C978DBA811B234C61ED4\g.pdb
0: kd> !v2p 00007ff6`f1db0000
Virtual address = 00007ff6`f1db0000
PagingMode: 4-level
DirBase: 00000001`f29c0000 ffff8040`20100800
PML4 Table @255 ffff8040`201007f8 = 0a000001`75bcc867 W U A
PageDirPointerTable @475 ffff8040`200ffed8 = 0a000001`6c8cd867 W U A
PageDirTable @398 ffff8040`1ffdbc70 = 0a000001`743ce867 W U A
PageTable @432 ffff803f`fb78ed80 = 86000001`40657025 XD R U A
Physical Address = 00000001`40657000
PFN@140657 ffffcf00`03c13050: ffffd404`e1b37a90 {b908974b`e2f00420} #14a1f1 0 0 0 Exist Proto
0: kd> dt nt!_MMPTE ffffd404`e1b37a90 u.Hard.
+0x000 u :
+0x000 Hard :
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y0
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y0
+0x000 PageFrameNumber : 0y000000000000000101000000011001010111 (0x140657)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y1010
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1
どちらのプロセスでも、g.exe がロードされたアドレスは 00007ff6`f1db0000 でした。両プロセス共に、マップしている物理アドレスは 00000001`40657000 であり、共有メモリになっていることが分かります。PFN レコードが参照しているのはプロトタイプ PTE であり、中身は Valid フラグが ON のハード PTE で、PFN は 0x140657 です。ここまでは前のセクションでデバッグした場合と同様です。
Forget ボタンをクリックしてから、子プロセスの仮想アドレスをもう一度確認します。
1: kd> .process /i ffffb90896264300
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff804`4e9fd0b0 cc int 3
0: kd> !v2p 00007ff6`f1db0000
Virtual address = 00007ff6`f1db0000
PagingMode: 4-level
DirBase: 00000001`f29c0000 ffff8040`20100800
PML4 Table @255 ffff8040`201007f8 = 0a000001`75bcc867 W U A
PageDirPointerTable @475 ffff8040`200ffed8 = 0a000001`6c8cd867 W U A
PageDirTable @398 ffff8040`1ffdbc70 = 0a000001`743ce867 W U A
PageTable @432 ffff803f`fb78ed80 = d404e1b3`7a900400 (inactive)
0: kd> !pte 00007ff6`f1db0000
VA 00007ff6f1db0000
PXE at FFFF8040201007F8 PPE at FFFF8040200FFED8 PDE at FFFF80401FFDBC70 PTE at FFFF803FFB78ED80
contains 0A00000175BCC867 contains 0A0000016C8CD867 contains 0A000001743CE867 contains D404E1B37A900400
pfn 175bcc ---DA--UWEV pfn 16c8cd ---DA--UWEV pfn 1743ce ---DA--UWEV not valid
Proto: FFFFD404E1B37A90
ここも同様に、PTE の P フラグが 0 になりましたが、!pte コマンドの出力が今までと違って "Proto: VAD" ではなく、"Proto: FFFFD404E1B37A90" と表示されています。念のため dt で確認します。
0: kd> dt nt!_MMPTE FFFF803FFB78ED80 u.Proto.
+0x000 u :
+0x000 Proto :
+0x000 Valid : 0y0
+0x000 DemandFillProto : 0y0
+0x000 HiberVerifyConverted : 0y0
+0x000 ReadOnly : 0y0
+0x000 SwizzleBit : 0y0
+0x000 Protection : 0y00000 (0)
+0x000 Prototype : 0y1
+0x000 Combined : 0y0
+0x000 Unused1 : 0y0000
+0x000 ProtoAddress : 0y110101000000010011100001101100110111101010010000 (0xd404e1b37a90)
ProtoAddress の値が 0xffffffff0000 ではなく 0xd404e1b37a90 です。これを 64-bit に符号拡張すると FFFFD404E1B37A90 になり、さっき出てきたプロトタイプ PTE のアドレスになります。つまり、無効になったソフト PTE が、VAD ではなくプロトタイプ PTE を直接参照しています。図示すると以下のようになります。
ここまで引っ張っておいたくせに最後の章短すぎでは・・・。
おわりに
OS は、CPU が無視するビットを利用してソフトウェア PTE を定義することができ、共有メモリのページング処理ではプロトタイプ PTE という構造を使って、ページフォルトの処理に使っているらしいことが分かりました。
これでようやく 12/24 の記事の準備ができます。冒頭で、イメージ ローダーの処理をネタにするという予告をしましたが、本記事最後のセクションでそのネタに少し触れました。そもそもここまで読んでくださっている方がいるのか不明ですが、もし読者の中で同じ疑問を持たれた方いましたら、是非 12/24 の記事を読んでください。その疑問とは、「親プロセスと子プロセスで、g.exe が同じ仮想アドレスにロードされていたのは偶然か否か?」です。