Windbg로 알아보는 PFN (_MMPFN)
PFN에 관해 좋은 글이 있어서,
Inside Windows Page Frame Number (PFN) - Part 1
; https://rayanfam.com/topics/inside-windows-page-frame-number-part1/
Inside Windows Page Frame Number (PFN) – Part 2
; https://rayanfam.com/topics/inside-windows-page-frame-number-part2/
저 글을 베껴 실습을 해보겠습니다. ^^
운영체제의 입장에서, 물리 메모리는 CPU가 정한 페이징 규칙을 따라
4KB 또는 2MB 단위로 관리를 합니다. 관리 방식은, 페이지 크기마다 매핑하는 데이터를 두는 형식인데요, 예를 들어 메모리가 32KB를 가진 시스템이라면 다음과 같은 구성을 갖는 배열을 유지합니다.
// Windows x86 non-PAE인 경우
[0]: 24바이트 PFN: [0 ~ 4KB)
[1]: 24바이트 PFN: [4KB ~ 8KB)
[2]: 24바이트 PFN: [8KB ~ 12KB)
[3]: 24바이트 PFN: [12KB ~ 16KB)
[4]: 24바이트 PFN: [16KB ~ 20KB)
[5]: 24바이트 PFN: [20KB ~ 24KB)
[6]: 24바이트 PFN: [24KB ~ 28KB)
[7]: 24바이트 PFN: [28KB ~ 32KB)
따라서 시스템이 가진 물리 RAM이 커질수록 오로지 저 PFN 데이터만을 위해 구성하는 배열(데이터베이스)의 크기도 증가합니다. 예를 들어, 1GB 물리 메모리를 가진 시스템이라면,
// 메모리 1GB, 4KB 페이지 크기, x86 non-PAE 모드에서의 PFN 데이터 크기 == 24바이트
(1024 * 1024 * 1024 / 4096) * 24바이트 == 6,291,456
약 6MB 정도의 메모리가 PFN을 위해 소비됩니다. 이게 작은 듯해도 32비트 시절에는 나름 부담이 되는 크기였는데요, 당시 x86 PAE 모드에서 16GB 물리 메모리를 가진 시스템에서는,
// 메모리 16GB, 4KB 페이지 크기, x86 PAE 모드에서의 PFN 데이터 크기 == 28바이트
(1024 * 1024 * 1024 * 16 / 4096) * 28바이트 == 117,440,512
순수하게 112MB 정도의 메모리가 커널 주소 공간(2GB)을 5%씩이나 점유하고 있었던 것입니다. 이로 인해, 16GB 이상의 RAM을 가진 PAE x86 시스템의 경우 사용자 메모리 주소 공간을 3GB로 올리는
IncreaseUserVA 옵션을 비활성화시킨 이유 중 하나가 되었다고 합니다. (즉, 커널 주소 공간으로 1GB만을 쓸 수 있는데 약 10% 이상의 공간이 순수하게 물리 메모리 관리를 위한 영역으로 쓰이니 그 외의 커널 데이터를 운영하기에는 부족했을 것입니다.)
참고로 PFN의 크기는 운영체제마다 다른데요, 현재의 Windows x64에서는 48(0x30) 바이트로 구성돼 있고 _MMPFN 구조체로 알려져 있습니다.
// Windows 10 x64 환경
1: kd> ?? sizeof(_MMPFN)
unsigned int64 0x30
1: kd> dt _MMPFN
nt!_MMPFN
+0x000 ListEntry : _LIST_ENTRY
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <unnamed-tag>
+0x008 PteAddress : Ptr64 _MMPTE
+0x008 PteLong : Uint8B
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <unnamed-tag>
+0x024 u5 : _MI_PFN_ULONG5
+0x028 u4 : <unnamed-tag>
그렇다면, 제가 사용하는 데스크톱 PC의 경우 메모리가 128GB니까,
(1024 * 1024 * 1024 * 128 / 4096) * 48 == 1,610,612,736 (약 1.5GB)
PFN을 위해 1.5GB 정도의 메모리가 소비되고 있을 것입니다. ^^;
자, 그럼 이쯤에서 용어 정리를 하나 해보자면, "Page"라고 하면 보통 운영체제 수준에서 다루는 가상 페이지를 의미하고, "(Page) Frame"이라고 하면 CPU 수준에서 다루는 실제 물리 메모리 페이지를 의미한다고 합니다.
윈도우 운영체제에서 PFN 배열의 시작 위치는 MmPfnDatabase 심벌로 풀이할 수 있습니다.
// Windows 10 x64 환경
2: kd> x nt!MmPfnDatabase
fffff807`1c11db60 nt!MmPfnDatabase = <no type information>
2: kd> dq nt!MmPfnDatabase L1
fffff807`1c11db60 ffffb100`00000000
// 또는,
2: kd> ? poi(nt!MmPfnDatabase)
Evaluate expression: -86861418594304 = ffffb100`00000000
위의 실습은 8GB 물리 메모리를 가진 가상 머신에서 한 것인데요, 따라서 PFN 데이터베이스의 크기는,
(1024 * 1024 * 1024 * 8 / 4096) * 48바이트 == 100,663,296 (약 96MB)
계산상으로는 96MB 정도라고 나오는 반면, windbg로 확인해 본 결과,
1: kd> !vm
...[생략]...
Boot Commit: 4743 ( 18972 Kb)
PFN Array Commit: 25600 ( 102400 Kb)
...[생략]...
나름 정렬 규칙이 있는 것인지 100MB 정도로 나옵니다. 그렇다면 대충 ffffb100`00000000 ~ ffffb100`06400000 정도의 가상 주소 범위에 PFN 데이터베이스가 자리 잡는 것으로 계산이 되고, 이를 (48바이트 구조체를 갖는 배열의) 인덱스 번호로 표현하면 0 ~ 2,184,533(0x215555) 범위의 PFN 번호를 가진다고 할 수 있습니다.
이 번호가 재미있는 것이, 그것이 곧 물리 메모리의 위치를 가리키는 것이나 다름없다는 점입니다. 가령, "PFN 1"이라고 하면 0x00000000`00001000 물리 주소를 가리키는 것과 같습니다.
예를 들어, windbg의 명령 중에 pfn 번호를 출력에 보여주는 !pte 명령어가 있는데요, 적당한 가상 주소로 _KPROCESS 구조체가 할당된 위치를 선택해 보겠습니다.
1: kd> !process 0 0 notepad.exe
PROCESS ffffd707bb8f3080
SessionId: 2 Cid: 3618 Peb: bb6bd65000 ParentCid: 11b8
DirBase: 1eae18000 ObjectTable: ffff9f80f7ec8040 HandleCount: 652.
Image: Notepad.exe
이에 대해 !pte 명령어를 사용하면,
1: kd> !pte ffffd707bb8f3080
VA ffffd707bb8f3080
PXE at FFFFC06030180D70 PPE at FFFFC060301AE0F0 PDE at FFFFC06035C1EEE0 PTE at FFFFC06B83DDC798
contains 0A000000F6A38863 contains 0A000000F6A3B863 contains 0A0000011878F863 contains 8A0000006238EB63
pfn f6a38 ---DA--KWEV pfn f6a3b ---DA--KWEV pfn 11878f ---DA--KWEV pfn 6238e CG-DA--KW-V
저렇게 Level 4 페이징에서의 개별 테이블을 담고 있는 PFN 번호를 알 수 있습니다. 다시 말하지만, PFN 번호를 안다는 것은 곧, 그 테이블이 위치한 물리 주소를 아는 것과 같으므로, 위의 PXE가 담고 있는 "pfn f6a38"은 곧 그다음 페이징 테이블이 0x00000000`f6a38000 물리 주소에 위치하고 있다는 것을 의미합니다. 그리고 여기서 개별 PTE 항목이 물리 주소와 직접적인 매핑 관계인 PFN 번호를 가질 수밖에 없는 이유는, CPU가 가상 주소를 물리 주소로 변환하는 과정이므로 그 내부에서 다시 가상 주소를 참조해야 한다면 무한 루프에 빠지기 때문입니다.
PFN 데이터베이스의 관리로 인해, 운영체제는 특정 물리 주소에 해당하는 Frame의 사용 여부를 쉽게 알 수 있습니다. 실제로 PFN의 상태별로 다음과 같은 식으로 연결 리스트를 구성해 놓고 있기 때문에,
[출처:
https://rayanfam.com/topics/inside-windows-page-frame-number-part1/]
위의 예에서 "Free"로 지정한 연결 리스트를 따라가면 현재 사용하고 있지 않은 페이지들의 정보를 빠르게 찾을 수 있습니다. (다시 말해, 물리 메모리를 할당해야 할 때 Free 링크에서 필요한 만큼 4KB 단위의 페이지를 빼내어 사용하면 되는 것입니다.)
개별 PFN 데이터가 담고 있는 데이터는 "!pfn" 명령어로 쉽게 조회할 수 있는데요, 가령 위의 !pte 출력에 나온 "pfn f6a38"의 데이터는 다음과 같이 확인할 수 있습니다.
1: kd> !pfn f6a38
PFN 000F6A38 at address FFFFB10002E3EA80
flink 00000000 blink / share count 00000006 pteaddress FFFFC06030180D70
reference count 0001 used entry count 0000 Cached color 0 Priority 0
restore pte 00000080 containing page 0007D5 Active M
Modified
"PFN 000F6A38 at address FFFFB10002E3EA80" 출력은 해당 PFN 항목을 담고 있는 가상 주소가 FFFFB10002E3EA80라는 것을 의미합니다. 당연히 그 주소는 (이전에 계산한) 8GB PFN 데이터베이스 범위인 ffffb100`00000000 ~ ffffb100`06400000 내에 속해 있습니다.
결국, 그 가상 주소를 덤프해 PFN 데이터를 확인할 수도 있습니다.
1: kd> dq FFFFB10002E3EA80 L6
ffffb100`02e3ea80 00000000`00000000 ffffc060`30180d70
ffffb100`02e3ea90 00000000`00000080 00000000`00000006
ffffb100`02e3eaa0 00000000`00560001 00400000`000007d5
// 혹은 공식으로도 가능
// ? (ffffb100`00000000 + (0xf6a38 * 0x30))
1: kd> dq ffffb100`00000000 + (0xf6a38 * 0x30) L6
ffffb100`02e3ea80 00000000`00000000 ffffc060`30180d70
ffffb100`02e3ea90 00000000`00000080 00000000`00000006
ffffb100`02e3eaa0 00000000`00560001 00400000`000007d5
// 또는, 구조체를 통해 덤프
1: kd> dt _MMPFN FFFFB10002E3EA80
nt!_MMPFN
+0x000 ListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0xffffc060`30180d70 ]
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <unnamed-tag>
+0x008 PteAddress : 0xffffc060`30180d70 _MMPTE
+0x008 PteLong : 0xffffc060`30180d70
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <unnamed-tag>
+0x024 u5 : _MI_PFN_ULONG5
+0x028 u4 : <unnamed-tag>
1: kd> dt _MMPFN FFFFB10002E3EA80 u2.Blink u2.ShareCount u2.LockNotused
nt!_MMPFN
+0x018 u2 :
+0x000 Blink : 0y0000000000000000000000000000000000000110 (0x6)
+0x000 ShareCount : 0y00000000000000000000000000000000000000000000000000000000000110 (0x6)
+0x000 LockNotUsed : 0y00000000000000000000000000000000000000000000000000000000000110 (0x6)
1: kd> dt _MMPFN FFFFB10002E3EA80 u3.e4.
nt!_MMPFN
+0x020 u3 :
+0x000 e4 :
+0x000 EntireField : 0x560001
1: kd> dt _MMPFN FFFFB10002E3EA80 u4.PteFrame
nt!_MMPFN
+0x028 u4 :
+0x000 PteFrame : 0y0000000000000000000000000000011111010101 (0x7d5)
PFN의 출력 중 containing page를 알아볼까요? ^^
1: kd> !pfn a0e32
PFN 000A0E32 at address FFFFEF0001E2A960
flink 00000A01 blink / share count 000000A1 pteaddress FFFFF2F9405F6A18
reference count 0001 used entry count 00A0 Cached color 0 Priority 5
restore pte 00A00080 containing page 046C16 Active M
Modified
이 값은 _MMPTE 구조체로 보면 u4.PteFrame에 해당하는데요,
1: kd> dt _MMPFN FFFFEF0001E2A960 u4.PteFrame
nt!_MMPFN
+0x028 u4 :
+0x000 PteFrame : 0y000000000000000001000110110000010110 (0x46c16)
1: kd> dq FFFFEF0001E2A960 L6
ffffef00`01e2a960 00000000`00000a01 fffff2f9`405f6a18
ffffef00`01e2a970 00000000`00a00080 00000000`000000a1
ffffef00`01e2a980 3300b559`05560001 00040000`00046c16
사실 PteAddress 가상 주소(위의 경우 FFFFF2F9405F6A18)의 물리 주소 Frame에 해당합니다.
1: kd> !vtop 0 FFFFF2F9405F6A18
Amd64VtoP: Virt fffff2f9405f6a18, pagedir 00000000055f2000
Amd64VtoP: PML4E 00000000055f2f28
Amd64VtoP: PDPE 00000000055f2f28
Amd64VtoP: PDE 00000000055f2010
Amd64VtoP: PTE 00000000bbe13fb0
Amd64VtoP: Mapped phys 0000000046c16a18
Virtual address fffff2f9405f6a18 translates to physical address 46c16a18.
1: kd> dq FFFFF2F9405F6A18 L1
fffff2f9`405f6a18 0a000000`a0e32867
// FFFFF2F9405F6A18의 물리 주소 == 46c16000(PFN 46c16) + a18 (offset) == 46c16a18
1: kd> dq /p 46c16a18 L1
00000000`46c16a18 0a000000`a0e32867
개인적으로, 궁금한 것이 하나 있는데요, PFN 데이터베이스(MmPfnDatabase)의 가상 주소가 ffffb100`00000000라고 나오는데, 정작 그것의 물리 주소를 알아내는 방법을 모르겠습니다. 처음에는 당연히 커널에서 할당했을 것이므로 System PTE 영역에 속할 것이라고 생각했는데,
1: kd> !process 0 0 system
PROCESS ffffd707b1abe040
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 007d5000 ObjectTable: ffff9f80e7a60d40 HandleCount: 4238.
Image: System
1: kd> !vtop 007d5000 ffffb100`00000000
Amd64VtoP: Virt 00000000ffffb100, pagedir 00000000007d5000
Amd64VtoP: PML4E 00000000007d5000
Amd64VtoP: PDPE 000000013fd27018
Amd64VtoP: zero PDPE
Virtual address ffffb100 translation fails, error 0xD0000147.
저렇게 실패로 나옵니다. 혹시 이에 대해 아시는 분은 덧글 부탁드립니다. ^^
마지막으로, 한때 PFN 데이터베이스의 크기를 다음의 공식으로 알 수 있었다고 합니다.
// On Windows (x86), how is the PFN database indexed?
// https://superuser.com/questions/1272962/on-windows-x86-how-is-the-pfn-database-indexed
?(poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase))/ @@(sizeof(nt!_MMPFN))
하지만 지금은 MmNonPagedPoolStart 심벌이 없어진 데다 근래의 Windows는 MmPfnDatabase에도 ASLR을 적용해 저런 식으로 심벌끼리의 차이로 크기를 알아내기가 어려워졌습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]