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 - (Ring 3 사용자 모드의) FS, GS Segment 레지스터

지난 글에서,

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

선형 주소로의 변환 시 관여하는 세그먼트 레지스터를 살펴봤는데요, 이번에는 남은 2개의 세그먼트인 FS, GS 레지스터를 마저 살펴보겠습니다.




FS, GS 레지스터의 최초 역할은 아래의 글에 설명이 나옵니다.

What is the "FS"/"GS" register intended for?
; https://stackoverflow.com/questions/10810203/what-is-the-fs-gs-register-intended-for

즉, 16비트 CPU 시절에 64KB 단위의 메모리 구역을 DS, ES로 나눠 지정할 수 있었던 것을 동시에 4개까지 서로 다른 64KB 영역을 지정할 수 있도록 추가로 제공한 것이 FS, GS였다고 합니다.

So, by using FS and GS, you could effectively address two more 64KB memory segments from your program without the need to change DS or ES registers whenever you need to address other segments than were loaded in DS or ES.


하지만, 32비트 시절로 오면서는 주소(Address) 선이 32비트로 넓어지면서 4개의 데이터 세그먼트 레지스터가 (그다지 절실하게) 필요하지는 않게 되었고, 따라서 운영체제 측에서 임의의 목적으로 사용할 수 있게 되었습니다.

실제로, 운영체제마다 FS, GS를 각기 다른 목적으로 사용할 수 있는데요, x64 윈도우 운영체제의 경우에는,

What is the GS register used for on Windows?
; https://stackoverflow.com/questions/39137043/what-is-the-gs-register-used-for-on-windows

FS 레지스터를 32비트 프로세스의 Thread Environment Block(TEB)를 가리키도록 하고 GS 레지스터는 64비트 프로세스의 TEB를 가리키는 용도로 사용합니다. (x86 윈도우의 경우에는 FS 레지스터만 사용하고 GS 레지스터는 사용하지 않습니다.)

실습으로 FS, GS가 어떤 값을 가지고 있는지 WinDbg를 통해 직접 확인해 볼 텐데요, 이를 위해 notepad.exe를 실행하고 WinDbg를 Attach시켜 rM 명령어를 사용하면,

// Windows 10 x64 환경의 notepad.exe를 디버그 연결

0:049> rM 8
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!DbgBreakPoint:
00007ffe`07814090 cc              int     3

저렇게 fs, gs 세그먼트의 값이 나옵니다. x64 윈도우에서 x64 프로세스(notepad.exe)를 실행했으니, 일단 GS 레지스터가 사용될 텐데요 그것의 GDT offset 값은 5로 계산되고,

2b == 00000000 00101011 (offset 0n5)

GDT로부터 offset 5에 해당하는 selector를 찾아보면,

// 세그먼트 1개 데이터 크기 8 바이트 * offset 5 == 40 (0x28)

4: kd> dg 28
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0028 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P  Nl 00000cf3

예상했던 것과는 달리 Base가 0으로 나옵니다. 말이 안 되죠? 가령, 흔히 하는 gs 레지스터 연산에서 gs:[0x30]을 접근하게 되는데, 그렇다면 Base 주소가 0이니 선형 주소로 00000000`00000030을 접근할 것이고, 당연히 해당 주소 영역은 윈도우에서 사용하지 않기 때문에 Access Violation 예외가 발생하게 됩니다.

0:049> db 00000000`00000030 L4
00000000`00000030  ?? ?? ?? ??                                      ????

물론, 이게 다가 아니겠죠? ^^




x64 CPU는 FS, GS에 대해 특별한 대우를 하는데요, 그 2개의 세그먼트에 대해서는 주소 계산을 GDT를 이용하지 않고 별도의 MSR(Model Specific Register) 값을 기반으로 합니다.

// https://sites.uclouvain.be/SystInfo/usr/include/asm/msr-index.h.html

#define MSR_FS_BASE                0xc0000100 /* 64bit FS base */
#define MSR_GS_BASE                0xc0000101 /* 64bit GS base */

가령, GS Base 주소는 MSR_GS_BASE(0xc0000101)를 통해 얻을 수 있는데요, 아쉽게도 "사용자 모드 (Ring 3)" 디버깅 상태로는 WinDbg에서 이 값을 확인할 수 없습니다.

// rdmsr은 Ring 0의 특권에서만 실행 가능한 명령어
// https://modoocode.com/en/inst/rdmsr

0:049> rdmsr 0xc0000101 
           ^ Bad register error in 'rdmsr 0xc0000101'

대신, 우회적으로 !teb 명령을 실행하면 GS base의 주솟값과 그와 관련된 데이터 내용을 확인할 수 있습니다.

// 사용자 모드 응용 프로그램의 디버깅 중에 실행 (Windows 10 x64 환경)

0:049> !teb
TEB at 000000061ed12000
    ExceptionList:        0000000000000000
    StackBase:            0000000620740000
    StackLimit:           000000062073c000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000061ed12000
    EnvironmentPointer:   0000000000000000
    ClientId:             000000000000cd9c . 0000000000011290
    RpcHandle:            0000000000000000
    Tls Storage:          0000000000000000
    PEB Address:          000000061ecab000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0

// !teb를 통해 알아낸 gs base 주소로부터 64바이트 값을 덤프
// 즉, gs:[0] ~ gs:[0x3f] 범위의 값을 8바이트 단위로 덤프
0:004> dq 000000061ed12000 L8
00000006`1ed12000  00000000`00000000 00000006`20740000
00000006`1ed12010  00000006`2073c000 00000000`00000000
00000006`1ed12020  00000000`00001e00 00000000`00000000
00000006`1ed12030  00000006`1ed12000 00000000`00000000

혹은, !teb가 출력한 주솟값을 대상으로 _TEB 구조체를 씌워 직접 모든 필드의 값을 확인하는 것도 가능합니다.

0:049> dt ntdll!_TEB 000000061ed12000 .
   +0x000 NtTib            : 
      +0x000 ExceptionList    : (null) 
      +0x008 StackBase        : 0x00000006`20740000 Void
      +0x010 StackLimit       : 0x00000006`2073c000 Void
      +0x018 SubSystemTib     : (null) 
      +0x020 FiberData        : 0x00000000`00001e00 Void
      +0x020 Version          : 0x1e00
      +0x028 ArbitraryUserPointer : (null) 
      +0x030 Self             : 0x00000006`1ed12000 _NT_TIB // GS base 주소
   +0x038 EnvironmentPointer : 
   +0x040 ClientId         : 
      +0x000 UniqueProcess    : 0x00000000`0000cd9c Void // Process ID
      +0x008 UniqueThread     : 0x00000000`00011290 Void // 스레드 ID
...[생략]...

위의 구조체에서 재미있는 점은 _TEB.Self 필드, 즉 gs:[0x30] 위치에 있는 값이 GS 세그먼트의 base 주소를 가리킨다는 점입니다. 이름이 의미하는 대로 자기 자신을 가리키는 포인터인데요, 실제로 !teb로 얻은 주솟값에서 0x30 위치의 값이 동일한 것을 확인할 수 있습니다.

0:049> dq 000000061ed12000 L8
00000006`1ed12000  00000000`00000000 00000006`20740000
00000006`1ed12010  00000006`2073c000 00000000`00000000
00000006`1ed12020  00000000`00001e00 00000000`00000000
00000006`1ed12030  00000006`1ed12000 00000000`00000000

다시 말해, 만약 rdmsr 명령어가 수행 가능했다면 MSR_GS_BASE(0xc0000101)를 통해 얻은 값이 0x000000061ed12000이었을 것으로 예측할 수 있습니다.

// 만약 WinDbg 사용자 모드 디버깅에서 아래의 명령어가 수행 가능했다면!

0:049> rdmsr 0xc0000101
msr[c0000101] = 00000006`1ed12000




그러고 보니, 예전 글에서 TEB를 구하기 위해 GS 레지스터를 사용한 적이 있었는데요,

x64 Visual C++에서 TEB 주소 구하는 방법
; https://www.sysnet.pe.kr/2/0/1387

따라서, C/C++에서 GS base 주소(MSR_GS_BASE)를 gs:[0x30]으로부터 구할 수 있고 결국 그 값이 gs:0의 위치를 의미한다는 것을 다음과 같이 테스트할 수 있습니다.

unsigned __int64* gsBaseAddress = (unsigned __int64*)__readgsqword(0x30);
assert(gsBaseAddress[6] == __readgsqword(0x30));

printf("GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x%p\n", gsBaseAddress);

아래는 이것을 좀 더 확장한 예제 코드입니다.

#include <stdio.h>
#include <windows.h>

extern "C"
{
    void FuncInThread1()
    {
        Sleep(1000);
    }

    void FuncInThread2()
    {
        Sleep(1000);
    }

    void PrintThreadInfo()
    {
        DWORD tid = GetCurrentThreadId();
        printf("\nThread ID: %d (0x%x)\n", tid, tid);
        for (int i = 0; i < 8 * 8; i += (8 * 2))
        {
            unsigned __int64 dqValue1 = __readgsqword(i);
            unsigned __int64 dqValue2 = __readgsqword(i + 8);
            printf("gs:[%04x] %016I64x %016I64x\n", i, dqValue1, dqValue2);
        }

        printf("\n");

        unsigned __int64 *gsBaseAddress = (unsigned __int64* )__readgsqword(0x30);
        {
            for (int i = 0; i < 8; i += 2)
            {
                unsigned __int64 dqValue1 = gsBaseAddress[i];
                unsigned __int64 dqValue2 = gsBaseAddress[i + 1];
                printf("gs:[%04x] %016I64x %016I64x\n", i * 8, dqValue1, dqValue2);
            }
        }

        {
            unsigned __int64* gsBaseAddress = (unsigned __int64*)__readgsqword(0x30);
            assert(gsBaseAddress[6] == __readgsqword(0x30));

            printf("GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x%p\n", gsBaseAddress);
        }
    }

    void ThreadProc1()
    {
        PrintThreadInfo();
        while (true)
        {
            FuncInThread1();
        }
    }

    void ThreadProc2()
    {
        PrintThreadInfo();
        while (true)
        {
            FuncInThread2();
        }
    }

    int main()
    {
        DWORD pid = ::GetProcessId(::GetCurrentProcess());
        printf("PID: %d (0x%x)\n", pid, pid);

        ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL);
        Sleep(1000);
        ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL);

        Sleep(-1);
    }
}

실행해 보면, 이런 식의 결과가 나옵니다.

Thread ID: 55672 (0xd978)
gs:[0000] 0000000000000000 0000005937f00000
gs:[0010] 0000005937efd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 0000005937998000 0000000000000000

gs:[0000] 0000000000000000 0000005937f00000
gs:[0010] 0000005937efd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 0000005937998000 0000000000000000
GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x0000005937998000

Thread ID: 49868 (0xc2cc)
gs:[0000] 0000000000000000 0000005938000000
gs:[0010] 0000005937ffd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 000000593799a000 0000000000000000

gs:[0000] 0000000000000000 0000005938000000
gs:[0010] 0000005937ffd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 000000593799a000 0000000000000000
GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x000000593799A000

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/26/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)
13641정성태6/11/20248655Linux: 71. Ubuntu 20.04를 22.04로 업데이트
13640정성태6/10/20248820Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
13639정성태6/8/20248426오류 유형: 906. C# MAUI - Android Emulator에서 "Waiting For Debugger"로 무한 대기
13638정성태6/8/20248513오류 유형: 905. C# MAUI - 추가한 layout XML 파일이 Resource.Layout 멤버로 나오지 않는 문제
13637정성태6/6/20248440Phone: 20. C# MAUI - 유튜브 동영상을 MediaElement로 재생하는 방법
13636정성태5/30/20248078닷넷: 2264. C# - 형식 인자로 인터페이스를 갖는 제네릭 타입으로의 형변환파일 다운로드1
13635정성태5/29/20248931Phone: 19. C# MAUI - 안드로이드 "Share" 대상으로 등록하는 방법
13634정성태5/24/20249409Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [1]
13633정성태5/22/20248931스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20248554Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20249311Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법 [1]
13630정성태5/19/20248861닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20249149개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20248538개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20248599오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20248857Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20248916닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20248712Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20248407닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20249196닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
13621정성태5/10/20248636오류 유형: 903. IISExpress - Failed to register URL "..." for site "..." application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
13620정성태5/9/20248540VS.NET IDE: 190. Visual Studio가 node.exe를 경유해 Edge.exe를 띄우는 경우
13619정성태5/7/20248851닷넷: 2259. C# - decimal 저장소의 비트 구조파일 다운로드1
13618정성태5/6/20248647닷넷: 2258. C# - double (배정도 실수) 저장소의 비트 구조파일 다운로드1
13617정성태5/5/20249469닷넷: 2257. C# - float (단정도 실수) 저장소의 비트 구조파일 다운로드1
13616정성태5/3/20248614닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...