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)
13718정성태8/27/20247413오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
13717정성태8/26/20247021VS.NET IDE: 192. Visual Studio 2022 - Windows XP / 2003용 C/C++ 프로젝트 빌드
13716정성태8/21/20246751C/C++: 167. Visual C++ - 윈도우 환경에서 _execv 동작 [1]
13715정성태8/19/20247349Linux: 78. 리눅스 C/C++ - 특정 버전의 glibc 빌드 (docker-glibc-builder)
13714정성태8/19/20246735닷넷: 2295. C# 12 - 기본 생성자(Primary constructors) (책 오타 수정) [3]
13713정성태8/16/20247445개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
13712정성태8/14/20247214개발 환경 구성: 720. Synology NAS - docker 원격 제어를 위한 TCP 바인딩 추가
13711정성태8/13/20248057Linux: 77. C# / Linux - zombie process (defunct process) [1]파일 다운로드1
13710정성태8/8/20247976닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용파일 다운로드1
13709정성태8/7/20247739닷넷: 2293. C# - safe/unsafe 문맥에 대한 C# 13의 (하위 호환을 깨는) 변화파일 다운로드1
13708정성태8/7/20247528개발 환경 구성: 719. ffmpeg / YoutubeExplode - mp4 동영상 파일로부터 Audio 파일 추출
13707정성태8/6/20247758닷넷: 2292. C# - 자식 프로세스의 출력이 4,096보다 많은 경우 Process.WaitForExit 호출 시 hang 현상파일 다운로드1
13706정성태8/5/20247881개발 환경 구성: 718. Hyper-V - 리눅스 VM에 새로운 디스크 추가
13705정성태8/4/20248148닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용 [2]파일 다운로드1
13704정성태8/2/20248102닷넷: 2290. C# - 간이 dotnet-dump 프로그램 만들기파일 다운로드1
13703정성태8/1/20247432닷넷: 2289. "dotnet-dump ps" 명령어가 닷넷 프로세스를 찾는 방법
13702정성태7/31/20247830닷넷: 2288. Collection 식을 지원하는 사용자 정의 타입을 CollectionBuilder 특성으로 성능 보완파일 다운로드1
13701정성태7/30/20248099닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용파일 다운로드1
13700정성태7/29/20247699디버깅 기술: 200. DLL Export/Import의 Hint 의미
13699정성태7/27/20248227닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입파일 다운로드1
13698정성태7/27/20248190닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리파일 다운로드1
13697정성태7/26/20247915닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리파일 다운로드1
13696정성태7/26/20247443오류 유형: 920. dotnet publish - error NETSDK1047: Assets file '...\obj\project.assets.json' doesn't have a target for '...'
13695정성태7/25/20247426닷넷: 2283. C# - Lock / Wait 상태에서도 STA COM 메서드 호출 처리파일 다운로드1
13694정성태7/25/20247891닷넷: 2282. C# - ASP.NET Core Web App의 Request 용량 상한값 (Kestrel, IIS)
13693정성태7/24/20247225개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...