Windbg - KPCR, KPRCB
프로세서가 사용자 모드의 코드를 실행하는 동안에는 GS 레지스터가 TEB 영역을 가리키지만, 커널 모드에서는 PCR 및 PRCB 값을 담은 영역을 가리킵니다.
// Fooling Windows about its internal CPU
// https://rayanfam.com/topics/fooling-windows-about-cpu/
// nt!_KPCR
// https://codemachine.com/articles/kernel_structures.html#KPCR
// (local kernel 디버깅이 아닌) live 커널 디버깅 모드 상태 (vCPU == 8개인 경우)
0: kd> dq gs:[0]
002b:00000000`00000000 fffff800`4e0c9fb0 fffff800`4e0c8000
002b:00000000`00000010 00000047`fdacf648 fffff800`4cf53000
002b:00000000`00000020 fffff800`4cf53180 fffff800`4cf53870
002b:00000000`00000030 00000047`fdc38000 fffff800`4e0c7000
002b:00000000`00000040 00000000`00000000 00000000`00000000
002b:00000000`00000050 00000000`00001000 00000000`00000000
002b:00000000`00000060 00000e10`00010001 00000000`00000000
002b:00000000`00000070 00000000`00000000 00000000`00000000
0: kd> ~7
7: kd> dq gs:[0]
002b:00000000`00000000 ffff8100`766b2fb0 ffff8100`766b1000
002b:00000000`00000010 00000047`fde7f228 ffff8100`766a2000
002b:00000000`00000020 ffff8100`766a2180 ffff8100`766a2870
002b:00000000`00000030 00000047`fdc40000 ffff8100`766b0000
002b:00000000`00000040 00000000`00000000 00000000`00000000
002b:00000000`00000050 00000000`00071000 00000000`00000000
002b:00000000`00000060 00000e10`00010001 00000000`00000000
002b:00000000`00000070 00000000`00000000 00000000`00000000
gs:[0]의 출력에서 어떤 오프셋에 KPCR과 KPRCB 값을 담고 있는지 확인하고 싶다면 각각 !pcr, !prcb 명령어로 알아낼 수 있는데요,
7: kd> !pcr
KPCR for Processor 7 at ffff8100766a2000:
Major 1 Minor 1
NtTib.ExceptionList: ffff8100766b2fb0
NtTib.StackBase: ffff8100766b1000
NtTib.StackLimit: 00000047fde7f228
NtTib.SubSystemTib: ffff8100766a2000
NtTib.Version: 00000000766a2180
NtTib.UserPointer: ffff8100766a2870
NtTib.SelfTib: 00000047fdc40000
SelfPcr: 0000000000000000
Prcb: ffff8100766a2180
Irql: 0000000000000000
IRR: 0000000000000000
IDR: 0000000000000000
InterruptMode: 0000000000000000
IDT: 0000000000000000
GDT: 0000000000000000
TSS: 0000000000000000
CurrentThread: ffff9485cf602080
NextThread: 0000000000000000
IdleThread: ffff9485cf602080
DpcQueue:
7: kd> !prcb
PRCB for Processor 7 at ffff8100766a2180:
Current IRQL -- 0
Threads-- Current ffff9485cf602080 Next 0000000000000000 Idle ffff9485cf602080
Processor Index 7 Number (0, 7) GroupSetMember 80
Interrupt Count -- 00006b89
Times -- Dpc 00000001 Interrupt 00000000
Kernel 00003191 User 00000009
그러니까, (물론 운영체제 버전에 따라서도 달라질 수 있지만) 대충 이렇게 정리할 수 있습니다.
// Windows 10 x64인 경우
gs:[18] == KPCR == ffff8100`766a2000 == Processor Control Region
gs:[20] == KPRCB == ffff8100`766a2180 == Processor Control Block
KPCR/KPRCB는 이름에서도 알 수 있듯이 물리 장치인 CPU를 소프트웨어, 즉 운영체제 측에서 추상화한 구조체인데요, 게다가 KPRCB는 KPCR 내에 포함된 구조체에 불과합니다. 그래서 일단 KPCR만 알아내면, KPRCB도 Prcb 필드를 이용해 확인할 수 있습니다.
7: kd> dt _KPCR ffff8100766a2000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : 0xffff8100`766b2fb0 _KGDTENTRY64
+0x008 TssBase : 0xffff8100`766b1000 _KTSS64
+0x010 UserRsp : 0x00000047`fde7f228
+0x018 Self : 0xffff8100`766a2000 _KPCR
+0x020 CurrentPrcb : 0xffff8100`766a2180 _KPRCB
+0x028 LockArray : 0xffff8100`766a2870 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : 0x00000047`fdc40000 Void
+0x038 IdtBase : 0xffff8100`766b0000 _KIDTENTRY64
+0x040 Unused : [2] 0
+0x050 Irql : 0 ''
+0x051 SecondLevelCacheAssociativity : 0x10 ''
+0x052 ObsoleteNumber : 0x7 ''
+0x053 Fill0 : 0 ''
+0x054 Unused0 : [3] 0
+0x060 MajorVersion : 1
+0x062 MinorVersion : 1
+0x064 StallScaleFactor : 0xe10
+0x068 Unused1 : [3] (null)
+0x080 KernelReserved : [15] 0
+0x0bc SecondLevelCacheSize : 0x400000
+0x0c0 HalReserved : [16] 0xd691cf40
+0x100 Unused2 : 0
+0x108 KdVersionBlock : (null)
+0x110 Unused3 : (null)
+0x118 PcrAlign1 : [24] 0
+0x180 Prcb : _KPRCB
// dt _KPRCB ffff8100766a2000+0x180
// dt _KPRCB ffff8100`766a2180
7: kd> dt _KPCR ffff8100`766a2000 Prcb.
nt!_KPCR
+0x180 Prcb :
+0x000 MxCsr : 0x1f80
+0x004 LegacyNumber : 0x7 ''
+0x005 ReservedMustBeZero : 0 ''
+0x006 InterruptRequest : 0 ''
+0x007 IdleHalt : 0x1 ''
+0x008 CurrentThread : 0xffff9485`cf602080 _KTHREAD
+0x010 NextThread : (null)
+0x018 IdleThread : 0xffff9485`cf602080 _KTHREAD
+0x020 NestingLevel : 0 ''
+0x021 ClockOwner : 0 ''
+0x022 PendingTickFlags : 0 ''
+0x022 PendingTick : 0y0
+0x022 PendingBackupTick : 0y0
+0x023 IdleState : 0 ''
+0x024 Number : 7
+0x028 RspBase : 0xfffff686`1fcbcc70
+0x030 PrcbLock : 0
...[생략]...
+0x9eac DbgMceNestingLevel : 0
+0x9eb0 DbgMceFlags : 0
+0x9eb4 PrcbPad139b : 0
+0x9eb8 CacheProcessorSet : [5] _KAFFINITY_EX
+0xa3e0 PrcbPad140 : [340] 0
+0xae80 PrcbPad140a : [8] 0
+0xaec0 PrcbPad141 : [512] 0
+0xbec0 RequestMailbox : [1] _REQUEST_MAILBOX
// 0xe10 == 3600 == 3.6GHz
7: kd> dt _KPRCB ffff8100`766a2180 MHz VendorString
nt!_KPRCB
+0x044 MHz : 0xe10
+0x8990 VendorString : [13] "AuthenticAMD"
기왕에 KPRCB까지 왔으니 그것의 필드 중에 DispatcherReadyListHead를 알아볼까요? ^^
Deep Dive into Everything - Dispatcher Database
; https://haewon83.tistory.com/m/125
위의 글에서 아주 친절하게 설명하고 있으므로 저는 그냥 그대로 실습만 해보겠습니다.
우선, windbg의 !thread 명령어로는 현재 문맥으로 지정된 CPU에 실행 중인 스레드를 확인할 수 있습니다.
// 0번 CPU에 실행 중인 스레드 확인 (전체 CPU에 실행 중인 모든 스레드를 확인하고 싶다면 "!running -it" 명령어 사용)
0: kd> !thread
THREAD fffff8071c14c700 Cid 0000.0000 Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap ffff9f80e7a59d50
Owning Process fffff8071c148f40 Image: Idle
Attached Process ffffd707b1abe040 Image: System
Wait Start TickCount 814672 Ticks: 1 (0:00:00:00.015)
Context Switch Count 399519 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 03:31:44.921
Win32 Start Address nt!KiIdleLoop (0xfffff8071b819cc0)
Stack Init fffff80718f13c70 Current fffff80718f13c00
Base fffff80718f14000 Limit fffff80718f0e000 Call 0000000000000000
Priority 0 BasePriority 0 IoPriority 0 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
fffff807`18f137e8 fffff807`22fc2b4c : ffffd707`b2f61890 00000000`00000000 00000000`00000000 00000000`00000000 : nt!DbgBreakPointWithStatus
fffff807`18f137f0 ffffd707`b2f61890 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0xfffff807`22fc2b4c
fffff807`18f137f8 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0xffffd707`b2f61890
보시면, callstack과 함께 Priority == 0이 나오고, 현재 스레드의 상태를 _KTHREAD.State 필드로 확인할 수 있습니다. (당연히 !thread 명령어 자체가 실행 중인 스레드를 보여주는 것이므로 Running을 의미하는 2가 나옵니다.)
0: kd> dt _KTHREAD fffff8071c14c700 State
nt!_KTHREAD
+0x184 State : 0x2 ''
/*
0 Init
1 Ready
2 Running
3 Standby
4 Terminate
5 Waiting
6 Transition
7 Deferred Ready
*/
반면 실행 중이 아닌, 대기 중인 스레드를 확인하고 싶다면 !ready 명령어를 사용하면 된다고 하는데요,
// Windows 10+ x64인 경우
0: kd> !ready
KSHARED_READY_QUEUE fffff8071c14b040: (00) ****------------------------------------------------------------
SharedReadyQueue fffff8071c14b040: No threads in READY state
Processor 0: No threads in READY state
Processor 1: No threads in READY state
Processor 2: No threads in READY state
Processor 3: No threads in READY state
KSHARED_READY_QUEUE ffffd707b1a97640: (00) ----****--------------------------------------------------------
SharedReadyQueue ffffd707b1a97640: No threads in READY state
Processor 4: No threads in READY state
Processor 5: No threads in READY state
Processor 6: No threads in READY state
Processor 7: No threads in READY state
절묘한 타이밍인 줄은 알 수 없지만 보는 바와 같이 Ready 상태로 대기하는 스레드가 모든 프로세서에 걸쳐 하나도 없다고 나옵니다. 저 출력이 올바른지에 대한 확인을 위해 KPRCB에 있는 32개의 고정 크기를 가진 DispatcherReadyListHead 배열을 살펴볼 수 있습니다. 가령 아래는 0번 프로세서의 DispatcherReadyListHead 목록을 보여주는데요,
0: kd> !prcb
PRCB for Processor 0 at fffff80717d8c180:
Current IRQL -- 2
Threads-- Current fffff8071c14c700 Next 0000000000000000 Idle fffff8071c14c700
Processor Index 0 Number (0, 0) GroupSetMember 1
Interrupt Count -- 001519ee
Times -- Dpc 00000004 Interrupt 0000000e
Kernel 000c6a5b User 000003f6
0: kd> dt _KPRCB fffff80717d8c180 DispatcherReadyListHead
nt!_KPRCB
+0x7f40 DispatcherReadyListHead : [32] _LIST_ENTRY [ 0xfffff807`17d940c0 - 0xfffff807`17d940c0 ]
0: kd> dt _KPRCB fffff80717d8c180 -a DispatcherReadyListHead
nt!_KPRCB
+0x7f40 DispatcherReadyListHead :
[00] _LIST_ENTRY [ 0xfffff807`17d940c0 - 0xfffff807`17d940c0 ]
[01] _LIST_ENTRY [ 0xfffff807`17d940d0 - 0xfffff807`17d940d0 ]
[02] _LIST_ENTRY [ 0xfffff807`17d940e0 - 0xfffff807`17d940e0 ]
[03] _LIST_ENTRY [ 0xfffff807`17d940f0 - 0xfffff807`17d940f0 ]
...[생략]...
[28] _LIST_ENTRY [ 0xfffff807`17d94280 - 0xfffff807`17d94280 ]
[29] _LIST_ENTRY [ 0xfffff807`17d94290 - 0xfffff807`17d94290 ]
[30] _LIST_ENTRY [ 0xfffff807`17d942a0 - 0xfffff807`17d942a0 ]
[31] _LIST_ENTRY [ 0xfffff807`17d942b0 - 0xfffff807`17d942b0 ]
첫 번째 배열의 경우 0xfffff807`17d940c0 - 0xfffff807`17d940c0으로 FLink와 BLink의 값이 같다는 점에서 ready 상태의 스레드가 없다는 것을 유추할 수 있습니다. 즉, "!ready" 명령어에서 확인한 것처럼 0번 CPU의 32개 LIST_ENTRY가 모두 비어 있다는 것이 확인됩니다.
여기서 32개인 이유는, 스레드에 대한 윈도우의 스케줄링 우선순위가 32단계로 결정돼 있기 때문입니다.
Scheduling Priorities
; https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
The priority levels range from zero (lowest priority) to 31 (highest priority).
아마도, 저 환경이 VM이라서 그런지 모르겠지만, 어쨌든 32개의 우선순위 큐에서 ready 상태의 스레드가 하나도 없고, 저 테스트를 나머지 7개의 CPU 문맥에서 모두 실행해 봐도 같다는 것을 확인할 수 있습니다.
참고로, PCR/PRCB는 MSR(model-specific register)을 이용해서도 알아낼 수 있습니다.
MSRs
; https://wiki.osdev.org/CPU_Registers_x86-64#MSRs
Intel® 64 and IA-32 Architectures Software Developer’s Manual
- B.1 ARCHITECTURAL MSRS (630 페이지)
https://www.intel.com/content/dam/support/us/en/documents/processors/pentium4/sb/253669.pdf
가령 IA32_GS_BASE 주소에 KPCR의 주소가 담겨 있는데요,
// 0xc0000101 == IA32_GS_BASE
// 아래는 7번 CPU가 실행 중인 프로세스/스레드 문맥의 KPRCB 주소를 출력
7: kd> rdmsr 0xc0000101
msr[c0000101] = ffff8100`766a2000
// 참고로, 원래 커널 모드의 GS base 주소를 담고 있는 MSR은 IA32_KERNEL_GS_BASE인데요,
// 왜? IA32_GS_BASE에 KPCR의 값이 담겨 있는지는 "swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터" 글에서 설명합니다.
이뿐만 아니라
tsc 값도 확인할 수 있고,
4: kd> rdmsr 0x10
msr[10] = 000182b2`54a00dd5
4: kd> rdmsr 0x10
msr[10] = 000182b5`bbeaf118
Extended Feature Enable Register로 알려진 값까지 구할 수 있습니다.
// 0xc0000080 == IA32_EFER (Extended Feature Enable Register)
5: kd> rdmsr 0xc0000080
msr[c0000080] = 00000000`00000d01
5: kd> .formats d01
Evaluate expression:
Hex: 00000000`00000d01
Decimal: 3329
Decimal (unsigned) : 3329
Octal: 0000000000000000006401
Binary: 00000000 00000000 00000000 00000000 00000000 00000000 00001101 00000001
Chars: ........
Time: Thu Jan 1 09:55:29 1970
Float: low 4.66492e-042 high 0
Double: 1.64474e-320
위의 출력 결과를 IA32_EFER 명세에 따라 해석해 보면,
1 : SCE (System Call Extensions)
0000000 : 0 (Reserved)
1 : LME (Long Mode Enable)
0 : (?)
1 : LMA (Long Mode Active)
1 : NXE (No-Execute Enable)
0 : SVME (Secure Virtual Machine Enable)
0 : LMSLE (Long Mode Segment Limit Enable)
0 : FFXSR (Fast FXSAVE/FXRSTOR)
0 : TCE (Translation Cache Extension)
16~63 : 0 (Reserved)
0번째 비트에 해당하는 SCE가 바로 (AMD64 K6 프로세서 이상부터 지원하는) user->kernel로의 전환을 보다 고속으로 실행할 수 있게 하는 syscall/sysret 명령어에 대한 허용 여부를 가리킵니다.
즉, 이 값이 1이면 syscall로 인한 진입 주소가 c0000082에 기록돼 있어야 합니다.
5: kd> rdmsr c0000082
msr[c0000082] = fffff806`08e12000
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]