Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN) [링크 복사], [링크+제목 복사],
조회: 2676
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 8개 있습니다.)
디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
; https://www.sysnet.pe.kr/2/0/13500

디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
; https://www.sysnet.pe.kr/2/0/13836

디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)
; https://www.sysnet.pe.kr/2/0/13844

디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
; https://www.sysnet.pe.kr/2/0/13845

디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형
; https://www.sysnet.pe.kr/2/0/13846

디버깅 기술: 209. Windbg로 알아보는 Prototype PTE
; https://www.sysnet.pe.kr/2/0/13848

디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
; https://www.sysnet.pe.kr/2/0/13849

디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터
; https://www.sysnet.pe.kr/2/0/13852




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/]
pfn_table_1.gif

위의 예에서 "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을 적용해 저런 식으로 심벌끼리의 차이로 크기를 알아내기가 어려워졌습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 12/17/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13859정성태1/9/20251850디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/20251877개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/20251899C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
13856정성태1/6/20251917디버깅 기술: 214. Windbg - syscall 단계까지의 Win32 API 호출 (예: Sleep)
13855정성태12/28/20242211오류 유형: 941. Golang - os.StartProcess() 사용 시 오류 정리
13854정성태12/27/20242554C/C++: 186. Golang - 콘솔 응용 프로그램을 NT 서비스를 지원하도록 변경파일 다운로드1
13853정성태12/26/20242429디버깅 기술: 213. Windbg - swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터
13852정성태12/25/20242333디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터파일 다운로드1
13851정성태12/23/20242282디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
13850정성태12/23/20242330오류 유형: 940. "Application Information" 서비스를 중지한 경우, "This file does not have an app associated with it for performing this action."
13849정성태12/20/20242516디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
13848정성태12/18/20242508디버깅 기술: 209. Windbg로 알아보는 Prototype PTE파일 다운로드2
13847정성태12/18/20242611오류 유형: 939. golang - 빌드 시 "unknown directive: toolchain" 오류 빌드 시 이런 오류가 발생한다면?
13846정성태12/17/20242616디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형파일 다운로드1
13845정성태12/16/20242902디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
13844정성태12/14/20242676디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)파일 다운로드1
13843정성태12/13/20242642오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20243031디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20243117오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20242946오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20242900오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20242901오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20243047디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20243204디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20243109오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20243112Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...