Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 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 - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경

이전에 가상 주소를 페이징 단계를 거쳐 물리 주소로 변환하는 것을 설명했는데요,

Windbg - x64 가상 주소를 물리 주소로 변환
; https://www.sysnet.pe.kr/2/0/13500

사실 그 중간 단계에 선형 주소(Linear address)로의 변환이 있긴 합니다.

Translating Virtual to Physical Address on Windows: Segmentation
; https://www.proteansec.com/reverse-engineering/translating-virtual-to-physical-address-on-windows-segmentation/

logical address -> [segmentation unit] -> linear addresss -> [paging unit] -> physical address

단지, 64비트가 보편화되면서 더 이상 주소 변환에 있어 세그먼트 레지스터가 의미 있는 역할을 하지 못하므로 사실상 몰라도 되는 지식이 됐습니다. 따라서 그렇게만 알고 더 읽지 않아도 되는데 ^^ 혹시나 그래도 궁금하시다면 가벼운 마음으로 보시면 되겠습니다.




실습을 위해 Windbg를 이용하면서 논리(가상) 주소를 선형 주소로 변환하는 과정을 살펴보겠습니다. 우선, 레지스터 정보 먼저 확인할 텐데요,

2: kd> r
rax=0000000000000000 rbx=ffff978ad4933410 rcx=ffff978ae9ac8b58
rdx=00000000001f0003 rsi=ffff978af7edf8e0 rdi=ffff978ae9ac8880
rip=fffff80683811960 rsp=ffffd485130475f8 rbp=ffff868232d7f080
 r8=0000000000000000  r9=0000000000000001 r10=fffff80683811960
r11=ffffd485130475d0 r12=0000000000000000 r13=ffff86822ce950c0
r14=ffff978ae9ac8b58 r15=0000000000000001
iopl=0         nv up ei ng nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040282
nt!ZwCreateEvent:
fffff806`83811960 488bc4          mov     rax,rsp

// 또는 세그먼트 레지스터만 보고 싶다면,

2: kd> rM 8
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040282
nt!ZwCreateEvent:
fffff806`83811960 488bc4          mov     rax,rsp

저렇게 CS, SS, DS, ES로 나오는 것이 바로 세그먼트 레지스터입니다.

CS: Code segment
SS: Stack segment
DS: Data segment
ES: Extra segment

이름이 의미하는 것처럼, CS는 코드 주소, SS는 스택 주소, DS/ES는 데이터 주소와 (기본적으로) 연결돼 동작합니다. 가령, 수행할 명령어를 가리키는 RIP 레지스터는 CS 레지스터와 연동하므로 위의 출력으로 따지면,

rip: fffff80683811960
cs (Code Segment): 10

실제 주소는 CS:[RIP]로 묶여서 표현됩니다. 따라서 [RIP] 주소에 영향을 미치는 세그먼트 레지스터의 값을 분석해야 하는데요, 값 자체는 다음과 같이 3개의 영역으로 분리가 됩니다.

0x10 16비트 == 0000000000010 0 00

00: protection
0: global/local descriptor (0 == GDT)
00000000 00010: GDT의 offset, (여기서는 2)

보는 바와 같이 코드 세그먼트는 그 자체로 주소 정보를 가지고 있지는 않고, 실제 정보를 담고 있는 GDT(Global Descriptor Table)에 대한 offset 값을 가지고 있습니다.

그런데 이름에도 유추할 수 있듯이 Global도 있지만 Local이 붙은 LDT(Local Descriptor Table)도 있습니다. 하지만, 윈도우 운영체제의 경우 LDT는 현재 사용하지 않는데요,

Understanding Win 7 x64 GDT/LDT
; https://community.osr.com/t/understanding-win-7-x64-gdt-ldt/48021

LDT was not used by WinNT at all except for Win16 NTVDM emulation.


64비트 윈도우부터 16비트 NTVDM이 아예 제거됐으므로 이제는 저 사실조차도 잊어버려도 됩니다.

결국 세그먼트가 가진 offset은 64비트 윈도우에선 모두 GDT만을 가리킵니다.




위의 "r" 명령에서 출력한 다른 세그먼트 레지스터의 오프셋도 마저 해석해 보면 이렇게 나옵니다.

cs: 0x10 (offset 2)
ss: 0x18  (offset 3)
ds, es: 0x2b (offset 5)

위치 값을 알았으니, 이제 GDT를 알아볼 차례군요. ^^

GDT의 위치와 그 크기는 어셈블리 명령어에 lgdt로 읽어낼 수 있습니다. LGDT는 32비트의 경우 48비트, 64비트의 경우에는 80비트의 데이터를 읽어내는데,

[32비트 CPU - 48비트]
0~15비트: GDT의 총 크기
16~48비트: 32비트 주소 공간 내에 있는 GDT의 시작 주소

[64비트 CPU - 80비트]
0~15비트: GDT의 총 크기
16~79비트: 64비트 주소 공간 내에 있는 GDT의 시작 주소

따라서, 근래의 64비트 환경이라면 80비트의 값을 반환합니다. 사실 저 어셈블리 명령어는 CPU의 GDTR이라는 특별한 레지스터의 값을 반환하는 것에 불과한데, 이유는 알 수 없지만 Windbg의 경우 마치 2개(GDTR, GDTL)의 레지스터가 있는 것처럼 나눠서 정보를 제공합니다.

// GDT의 위치
2: kd> ? gdtr // 또는, "? @gdtr", "r @gdtr" 가능
Evaluate expression: -45622302466128 = ffffd681`bade1fb0

// GDT의 크기
2: kd> ? gdtl // 또는, "? @gdtl", "r @gdtl" 가능
gdtl=0057

// 또는, "rM" 명령어로 한 번에 가능 (LDT는 사용하지 않으므로 0 출력)
2: kd> rM 100
gdtr=ffffd681bade1fb0   gdtl=0057 idtr=ffffd681baddf000   idtl=0fff tr=0040  ldtr=0000

그런데 특이하게도 크기 값이 0x57로 돼 있는데 이것은 실제 값에서 1을 뺀 값이 나온 것입니다. 왜냐하면 16비트 최댓값인 0xFFFF가 65,535까지 표현할 수 있는데 GDT 항목이 8192개까지 가능하므로 8192 * 8바이트 = 65,536바이트가 되기 때문에 1이 적을 수밖에 없습니다. (따라서, GDTL을 크기라기보다는 0 ~ 0x57의 범위로 해석해야 합니다.)

저렇게 GDT의 위치와 크기를 알아냈으면 이제 덤프를 해볼까요? ^^

// gdtl == 0x57이므로 0x58개의 바이트를 덤프합니다.

2: kd> db gdtr L58

// 하지만 GDT 내의 항목의 크기가 8바이트이므로, dq로 11(0x0b) 개를 덤프하면 됩니다.

2: kd> dq /c1 gdtr LB
ffffd681`bade1fb0  00000000`00000000
ffffd681`bade1fb8  00000000`00000000
ffffd681`bade1fc0  00209b00`00000000  // (offset 2, 위의 출력에서 CS 레지스터의 값)
ffffd681`bade1fc8  00409300`00000000  // (offset 3, 위의 출력에서 SS 레지스터의 값)
ffffd681`bade1fd0  00cffb00`0000ffff
ffffd681`bade1fd8  00cff300`0000ffff  // (offset 5, 위의 출력에서 DS, ES 레지스터의 값)
ffffd681`bade1fe0  0020fb00`00000000
ffffd681`bade1fe8  00000000`00000000
ffffd681`bade1ff0  ba008bde`00000067
ffffd681`bade1ff8  00000000`ffffd681
ffffd681`bade2000  0040f300`0000bc00

위의 예에서 CS 레지스터가 가리키는 항목을 해석하면 이렇게 됩니다.

CS: 0x10 (offset 2) == 00209b00`00000000

00000000 00100000 10011011 00000000 ` 00000000 00000000 00000000 00000000

00000000 00000000: Limit (0 ~ 15비트)
00000000 00000000: Base (16 ~ 31비트)
00000000: Base (32 ~ 39비트)
10011011: Access Byte (40 ~ 47비트)
0000: Limit (48 ~ 51비트)
0010: Flags (52 ~ 55비트)
00000000: Base (56 ~ 63비트)

저 64비트 중 Base 주소가 32비트, Limit가 20비트로 구성돼 있습니다. 하지만, 이런 해석과 무관하게 64비트 모드에서는 Base, Limit를 기본적으로 무시하게 돼 있습니다.

// https://wiki.osdev.org/Global_Descriptor_Table

In 64-bit mode, the Base and Limit values are ignored, each descriptor covers the entire linear address space regardless of what they are set to.


물론, 32비트 모드에서는 기준 주소를 논리 주소에 더하는 식으로 선형 주소를 구하게 됩니다. 위의 경우, Base 주소가 0이므로 결국 사용자의 주소가 그대로 선형 주소와 같게 됩니다.

이야기가 다시 처음으로 돌아가지만, 바로 저런 이유 때문에 결국 가상 주소를 물리 주소로 변환하는 과정에 세그먼트 레지스터로 인한 선형 주소 계산이 필요 없게 된 것입니다.




비록 세그먼트 레지스터가 주소 변환에는 영향을 미치지 않지만 그래도 일부 기능이 남아 있는데요, 이를 위해 Access Byte와 Flags를 마저 해석해 볼 필요가 있습니다.

Access Byte: 10011011  // https://wiki.osdev.org/Global_Descriptor_Table

1: A (Accessed bit)
1: RW (Readable/Writable bit)
0: DC (Direction/Conforming bit)
1: E (Executable bit) 1 == Code Segment, 0 == Data Segment
1: S (Descriptor type) 1 == Code/Data Segment, 0 == System Segment (TSS, LDT, etc.)
00: DPL (Descriptor Privilege Level) 00 == Kernel, 11 == User
1: P (Present bit) 1 == Present, 0 == Not Present

Flags 0010  // https://wiki.osdev.org/Global_Descriptor_Table

0: Reserved
1: L (Long-mode code flag)
0: DB (Size flag) 0 == 16-bit, 1 == 32-bit
0: G (Granularity flag) 0 == 1 byte, 1 == 4KB

"E" 비트야 뭐 Code/Data 세그먼트를 구분하는 의미라니 직관적으로 이해가 됩니다. DPL의 경우가 좀 흥미로운데요, 본문에서 "r" 명령어로 출력한 레지스터의 경우 커널 디버깅을 하는 중에 덤프한 것이라 DPL == 0 값이 나왔습니다.

사실 이것도 32비트에서 보다 더 의미가 있었는데요, 가령 사용자 모드 프로그램에서는 DPL 값이 3으로 설정된 데이터 세그먼트를 사용하게 만들고, 그것의 Limit를 (4GB 중 하위) 2GB로 설정함으로써 선형 주소로의 변환 단계 중에 이미 2GB 이상의 주소를 접근할 수 없게 만들었습니다.

(물론, 64비트로 와서는 Limit가 의미 없어졌으므로 DPL에 따른 제한만 남았습니다.)

그나저나 위에서 GD 항목이 담은 Limit가 20비트라고 했는데요, 이것은 1MB에 불과합니다. 상식적으로 (32비트 시절에) 4GB 주소 공간에 대해 base와 limit를 설정하려면 모두 32비트가 필요한데 20비트로는 당연히 정상적인 Limit를 표현할 수 없습니다. 바로 여기서 부족한 12비트를 보충하는 곳이 "Flags"에 지정된 G 플래그입니다. 그 값이 1로 설정되면 Limit의 20비트가 나타내는 숫자 단위가 4KB가 돼 결국 12비트가 보충되므로 32비트 주소 공간을 (4KB 단위로) 지정할 수 있습니다.

참고로, 지금까지 필드 단위의 해석 방법을 설명했지만 GDT 해석을 저렇게 직접 할 필요 없이 windbg라면 단순히 dg 명령어로 쉽게 덤프할 수 있습니다.

2: kd> dg 0 50
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0000 00000000`00000000 00000000`00000000 <Reserved> 0 Nb By Np Nl 00000000
0008 00000000`00000000 00000000`00000000 <Reserved> 0 Nb By Np Nl 00000000
0010 00000000`00000000 00000000`00000000 Code RE Ac 0 Nb By P  Lo 0000029b // Code Segment for Kernel
0018 00000000`00000000 00000000`00000000 Data RW Ac 0 Bg By P  Nl 00000493 // Stack Segment for Kernel
0020 00000000`00000000 00000000`ffffffff Code RE Ac 3 Bg Pg P  Nl 00000cfb
0028 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P  Nl 00000cf3 // DS, ES
0030 00000000`00000000 00000000`00000000 Code RE Ac 3 Nb By P  Lo 000002fb
0038 00000000`00000000 00000000`00000000 <Reserved> 0 Nb By Np Nl 00000000
0040 ffffffff`bade0000 00000000`00000067 TSS32 Busy 0 Nb By P  Nl 0000008b
0048 00000000`0000ffff 00000000`0000d681 <Reserved> 0 Nb By Np Nl 00000000
0050 00000000`00000000 00000000`0000bc00 Data RW Ac 3 Bg By P  Nl 000004f3




위에서는 제가 실습을 커널 디버깅 중에 덤프한 레지스터로 했는데, 만약 사용자 모드 응용 프로그램을 windbg로 디버깅하고 있다면 대충 이런 식의 세그먼트 레지스터 값이 나옵니다.

// 이 명령은 사용자 모드의 코드를 디버깅하는 중에 실행

0:039> rM 8
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

각각의 세그먼트 레지스터 값과 offset을 계산해 보면,

cs: 0x33 ==> offset 6
ss, ds, es: 0x2b ==> offset 5

5번 항목은 이전에 커널 모드의 것과 같으므로 생략하고 6번은 이런 식으로 출력이 됩니다.

// 이 명령은 커널 모드 디버깅에서 실행

6: kd> dg 30
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0030 00000000`00000000 00000000`00000000 Code RE Ac 3 Nb By P  Lo 000002fb

// Code 영역이므로 Executable 유형이고 
// Privilege Level이 3이므로 User 모드에서 접근 가능

정리해 보면, 64비트로 오면서 그나마 DPL(Descriptor Privilege Level)이나 RE(Readable/Executable) 등의 일부 정보를 제외하고는 세그먼트가 거의 쓸모 없어졌다는 것을 알 수 있습니다.




마치기 전에 부가적으로 CR4 레지스터의 구조를 소개합니다. ^^

// Windows 11 x64에서 수행

2: kd> r cr4
cr4=0000000000350ef8

350ef8 == 00000000 00110101 00001110 11111000

0: VME
0: PVI
0: TSD
1: DE
1: PSE (Page Size Extensions)
1: PAE
1: MCE
1: PGE

0: PCE
1: OSFXSR
1: OSXMMEXCPT
01: Reserved?
0: VMXE
0: SMXE
0: Reserved?

1: FSGSBASE
0: PCIDE
1: OSXSAVE
0: Reserved?
1: SMEP
1: SMAP
00000000 0: Reserved

cr4_register_1.png

아울러 "r" 레지스터 명령의 출력을 종류별로 필터링하는 "rM" 명령어의 옵션도 눈도장 한번 찍어보시고. ^^

0: kd> rm ?
       1 - Integer state (32-bit) or
       2 - Integer state (64-bit), 64-bit takes precedence
       4 - Floating-point state
       8 - Segment registers
      10 - MMX registers
      20 - Debug registers and, in kernel, CR4
      40 - SSE XMM registers
     200 - AVX YMM registers
     400 - AVX YMM Integer registers
     800 - AVX XMM Integer registers
    1000 - AVX-512 ZMM registers
    2000 - AVX-512 ZMM Integer registers
    4000 - AVX-512 Opmask registers
    8000 - Shadow Stack Registers
    10000 - AMX registers
      80 - CR0, CR2 and CR3
     100 - Descriptor and task state




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/27/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)
13883정성태2/12/2025189닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/2025582스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/2025894닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/2025869오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20251107오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/2025987오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20251082닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20251274닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20251264스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20251211스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20251107디버깅 기술: 217. WinDbg - PCI 장치 열거
13872정성태1/23/20251089오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20251201Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20251274개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20251329개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20251233Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
13867정성태1/17/20251396오류 유형: 943. Hyper-V에 Windows 11 설치 시 "This PC doesn't currently meet Windows 11 system requirements" 오류
13866정성태1/16/20251359개발 환경 구성: 739. Windows 10부터 바뀐 device driver 서명 방법
13865정성태1/15/20251477오류 유형: 942. C# - .NET Framework 4.5.2 이하의 버전에서 HttpWebRequest로 https 호출 시 "System.Net.WebException" 예외 발생
13864정성태1/15/20251427Linux: 114. eBPF를 위해 필요한 SELinux 보안 정책
13863정성태1/14/20251320Linux: 113. Linux - 프로세스를 위한 전용 SELinux 보안 문맥 지정
13862정성태1/13/20251282Linux: 112. Linux - 데몬을 위한 SELinux 보안 정책 설정
13861정성태1/11/20251792Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/20251740디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/20251850디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...