스택

J·2025년 10월 19일

테스트

목록 보기
8/11
post-thumbnail

※ 개인적인 생각이 추가되어 정확하지 않은 정보일 수 있다. 자꾸 까먹어서 기억을 위해 기록

스택

  • 함수 호출과 지역변수 관리용으로 사용되는 메모리 주소 공간
  • 높은 주소 -> 낮은 주소의 순서로 사용된다.
  • ARM64 / X86 / X64 아키텍처의 경우 스택의 기본 크기는 1MB이다 (설정을 통해 변경 가능하다.)

스택의 크기

  • 기본 RESERVE되는 페이지의 크기는 1MB만큼이며, COMMIT되는 크기는 4KB이다.

공식 문서 : https://learn.microsoft.com/ko-kr/cpp/build/reference/stack-stack-allocations?view=msvc-170

공식 문서에서 정의한 페이지 상태

페이지 상태의미
MEM_COMMIT (0x1000)메모리 또는 디스크의 페이징 파일에서 실제 스토리지가 할당된 커밋된 페이지를 나타냅니다.
MEM_FREE (0x10000)호출 프로세스에 액세스할 수 없고 할당할 수 있는 사용 가능한 페이지를 나타냅니다. 무료 페이지의 경우 AllocationBase, AllocationProtect, Protect 및 Type 멤버의 정보는 정의되지 않습니다.
MEM_RESERVE (0x2000)실제 스토리지가 할당되지 않고 프로세스의 가상 주소 공간 범위가 예약된 예약된 페이지를 나타냅니다. 예약된 페이지의 경우 보호 멤버의 정보가 정의되지 않습니다.

VirtualQuery를 통해 얻은 _MEMORY_BASIC_INFORMATION 구조체의 Protect 멤버는 페이지 상태가 MEM_FREE / MEM_RESERVE 상태에서는 정의되지 않는다.

메모리 보호 상수

https://learn.microsoft.com/ko-kr/windows/win32/memory/memory-protection-constants

스택의 초기 크기 확인

32비트 실행



64비트 실행


Guard 부분은 32비트 기준 2Page, 64비트 기준 3Page로 동작하는 듯.

공식 문서는 초기 커밋 값이 4KB라고 나와있는데 내가 측정하면 4KB이상의 값들이 COMMIT되어있어서 어떻게하면 4KB COMMIT을 확인할 수 있을까? 싶어서 스레드를 생성만 하고 그 상태를 측정해보았다.

새로 생성한 스레드 ID : 0x6f58
main 스레드 ID : 0xa1fc

  1. 스레드를 생성하고 실행 전의 메모리 주소를 Windbg를 통해 확인

  2. 확인 결과

unKnown으로 표시된 부분에 새로 생성되는 스레드에 관한 정보가 들어올 것 같은 느낌이 든다.

주황색으로 칠된 부분은 완전히 내가 측정한 스택의 구조와 비슷하게 가고 있지 않은가?? 그래서

  1. 스레드를 실행시킨 후 저 정보를 다시 확인해보았다.

TEB에 대한 포인터는 노랑 + 연보라로 칠한 부분,
스택에 관한 부분은 주황색 부분으로 확인할 수 있었고

스택 구조를 담아놓은 unknown 상태에서 Commit된 메모리가 0x1000 (4KB) 인 것을 확인했으며, 스레드 초기화 과정에서 스택을 4KB 이상 사용해 GUARD를 넘어서서 자동으로 확장되는 것이 아닐까?? 라는 생각을 해본다.

COMMIT 확장

  • PAGE_GUARD 속성의 페이지에 접근을 시도하면 시스템은 그 사실을 알게 되고, 이때 시스템은 가드 페이지 이하에 추가적으로 페이지를 커밋하고 현재 가드 페이지의 PAGE_GUARD 속성을 해제하고 커밋된 페이지에 대해서 PAGE_GUARD 속성을 추가한다.

  • 스레드가 가장 마지막의 가드 페이지에 접근하게 되면 시스템은 EXCEPTION_STACK_OVERFLOW 예외를 발생시킨다.

확인을 위해 스택이 Reserve된 페이지를 끝까지 늘려보도록 하였다.

마지막 1개의 Reserve가 남은 상태에서 Guard Page에 접근하게되면 StackOveflow가 발생하는 것을 확인하였다.

그 외에..

Test 1 : Guard가 2개 있는데
[낮은 주소]
[Guard1]
[Guard2]
[Commit]
[높은 주소]

만약 Guard2를 건드리지 않고 Guard1을 건드리면 Guard2도 커밋이 될까? 또 다음 Guard는 몇 개가 올라갈까? 테스트해봤지만.. 아직 잘 모르겠다 더 공부해야할듯

불확실한.. 생각...100%
  1. 기본 형태 출력

  1. 코드에서 임의로 Guard1만 건드리도록 설정

  2. 결과

로 동작하는줄 알았지만..

Windbg로도 계속 수정해보고 확인해봤는데 지금 위의 1,2,3번처럼 생각하면 계속 수정했을 때 내가 생각하는 것처럼 동작하지 않는다... 이런 행동은 결과를 100% 보장할 수 없는 것 같다.... 흠... 그래서 이런 상황일 때의 동작은 UB로 처리되는걸까 궁금하다..

Windbg도 명령어들 계속 찾아보면서 하고 있는데 빨리 활용할 수 있도록 하자.. 너무 어렵다.

코드

 #include <iostream>
#include <Windows.h>
#include <unordered_map>
#include <string>
#include <process.h>
#include <conio.h>

#define PAGE_SIZE (0x00001000)
#define STACK_SIZE (0x00100000)
#define PAGE_SIZE_MASK (0xfffff000)
#define GRANULARITY_BOUNDARY_MASK (0xffff0000)

INT_PTR stackBase;
std::unordered_map<void*, MEMORY_BASIC_INFORMATION> p;

void TEST1();
std::string GetPageStateStr(DWORD state)
{
	switch (state)
	{
		case 0x1000:return "            MEM_COMMIT";
		case 0x10000:return "              MEM_FREE";
		case 0x2000:return "           MEM_RESERVE";
		default:return "         INVALID_STATE";
	}
}

std::string GetPageProtectionConstantsStr(DWORD protect)
{
	// 메모리 보호 상수
	https://learn.microsoft.com/ko-kr/windows/win32/memory/memory-protection-constants

	DWORD protectionConstant = (protect & 0xff);

	std::string ret{""};
	switch (protectionConstant)
	{
		// PAGE_READONLY
		// PAGE_READWRITE
		// PAGE_WRITECOPY
		// 의 DEP (Data Execution Prevention) : https://learn.microsoft.com/ko-kr/windows/win32/memory/data-execution-prevention 
		// Windows에서 기본 옵션은 [DEP 켜져있음]

	case 0x00:ret += "NONE"; break;
	case 0x01: ret += "NO_ACCESS"; break; //			모든 접근 => 액세스 위반
	case 0x02: ret += "PAGE_READONLY"; break;//			쓰기, ●DEP 사용 시 : 실행 => 액세스 위반	[읽기 가능 속성] 
	case 0x04: ret += "PAGE_READWRITE"; break;//		●DEP 사용 시 : 실행 => 액세스 위반			[읽기, 쓰기 가능 속성]
	case 0x08: ret += "PAGE_WRITECOPY"; break;//		●DEP 사용 시 : 실행 => 액세스 위반			[읽기, 복사 가능 속성] 
	
	case 0x10: ret += "PAGE_EXECUTE"; break;//			쓰기 => 액세스 위반							[실행 가능 속성]
	case 0x20:ret += "PAGE_EXECUTE_READ"; break;//		쓰기 => 액세스 위반							[실행 / 읽기 가능 속성] 
	case 0x40: ret += "PAGE_EXECUTE_READWRITE"; break; //											[실행 / 읽기 / 쓰기 가능 속성]
	case 0x80: ret += "PAGE_EXECUTE_WRITECOPY"; break; //											[실행 / 읽기 / 복사 가능 속성]

	// PAGE_TARGETS_INVALID와 PAGE_TARGETS_NO_UPDATE에 대한 부분은 잘 모르겠다.
	default: ret += "INVALID_PROTECTION_C";  break;
	}

	int s = 22 - ret.length();

	for (int i = 0; i < s; i++)
	{
		ret += " ";
	}

	DWORD protectionQualifier = (protect & 0x0f00);

	switch (protectionQualifier)
	{
	case 0x100: ret += " | PAGE_GUARD"; break; // GUARD 속성  => 접근 시 STATUS_GUARD_PAGE_VIOLATION 예외를 발생 + 해당 가드 속성을 제거
	case 0x200: ret += " | PAGE_NOCACHE"; break; 
	case 0x400: ret += " | PAGE_WRITECOMBINE"; break;
	default:break;
	}

	return ret;
}

std::string GetKilloBytes(size_t byteSize)
{
	size_t kb = byteSize / 1024;
	
	size_t b = byteSize % 1024;

	std::string ret = "[ " + std::to_string(kb) + "KB " + std::to_string(b) + "BYTE ]";
	
	return ret;
}

unsigned WINAPI TestThread(LPVOID lparam)
{
	return 0;
}
int main()
{
	HANDLE h = (HANDLE)_beginthreadex(nullptr, 0, TestThread, nullptr, CREATE_SUSPENDED, nullptr);

	if (nullptr == h)
		return 0;

	printf("Created Thread ID : 0x%x\n", GetThreadId(h));

	printf("ID : 0x%x\n", GetCurrentThreadId());

	ResumeThread(h);
	// TEB 정보를 얻어옴.
	PNT_TIB tib = (PNT_TIB)NtCurrentTeb();
	// https://ko.wikipedia.org/wiki/Win32_%EC%8A%A4%EB%A0%88%EB%93%9C_%EC%A0%95%EB%B3%B4_%EB%B8%94%EB%A1%9D
	// TIB(TEB) 정보, Windbg로도 확인할 수 있었음.
	// 
	// +--------------------+
	// |	Page속성(Guard)	|
	// |					|
	// |--------------------|
	// |	StackLimit		|
	// |					|
	// |					|
	// |	StackBase		|
	// +--------------------+
	// 
	// 스택 베이스 / 스택의 바닥 (높은 주소)
	stackBase = (INT_PTR)tib->StackBase;

	// 스택 한계 / 스택의 천장 (낮은 주소)
	//printf("StackLimit : 0x%p\n", (void*)stackLimit);
	//std::cout << "초기 커밋 사이즈: "
	//	<< ((INT_PTR)stackBase - (INT_PTR)stackLimit) << " bytes\n";

	// 기본 스택(1MB)의 구조 파악을 위해 스택의 낮은 주소 -> 높은 주소로의 페이지 속성을 확인한다.



	while (1)
	{
		
		if (_kbhit())
		{
			char c = _getch();
			if ('q' == c || 'Q' == c)
				break;
		}
		
		INT_PTR stackStartPos = stackBase - (STACK_SIZE);
		INT_PTR stackLimit = (INT_PTR)tib->StackLimit;

		int Cnt = 0;

		printf("StackStartPos : 0x%p\n", (void*)stackStartPos);
		printf("StackBase : 0x%p\n", (void*)stackBase);
		printf("StackLimit : 0x%p\n", (void*)stackLimit);


		int remainingSize = 1024 * 1024;
		
		// 자동 페이지 확장을 위한 변수
		int* pAuto = (int*)0xffffffff;

		while (stackStartPos < stackBase)
		{
			MEMORY_BASIC_INFORMATION mbi{};
			VirtualQuery((void*)stackStartPos, &mbi, sizeof(mbi));

			// 얻은 페이지의 속성이 얼마나 연속적인가를 나타낸다. (해당 크기만큼의 페이지는 동일한 속성을 가진다.)

			remainingSize -= mbi.RegionSize;

			if (remainingSize < 0)
				DebugBreak();

			printf("-----------------------------------------------\n");
			printf("Count			: %d\n", Cnt++);
			printf("Address			: 0x%p\n", (void*)stackStartPos);
			printf("Page State		: %s\n", GetPageStateStr(mbi.State).c_str());
			printf("Page Protection		: %s\n", GetPageProtectionConstantsStr(mbi.Protect).c_str());
			printf("Region Size		: %s\n", GetKilloBytes(mbi.RegionSize).c_str());
			printf("Remaining Size		: %s\n", GetKilloBytes(remainingSize).c_str());
			printf("\n\n");

			pAuto = (int*)stackStartPos;
			
			// breakPoint 걸어서 pA조작
			{
				int testA = 3;
				int* pA = &testA;

				// PAGE_PROTECTION이 GUARD인 경우에 바꾸면 다시 처음부터 스택 정보 출력.
				*pA = 43;

				if (pA != &testA)
				{
					TEST1();
					break;
				}
			}

			stackStartPos += mbi.RegionSize;

		}

		bool bAutoEx = false;
		
		// 자동으로 확장을 원하는 경우 여기도 breakPoint 걸어서 조작
		if(bAutoEx)
		*(--pAuto) = 4;

		Sleep(100);
		
		system("cls");
	}
	
	CloseHandle(h);

	return 0;
}

void TEST1()
{
	// TEST를 위해서 STACK 시작지점부터 다시 메모리 체크.
	INT_PTR stackStartPos = stackBase - (STACK_SIZE);
	int Cnt = 0;

	printf("StackStartPos : 0x%p\n", (void*)stackStartPos);
	printf("StackBase : 0x%p\n", (void*)stackBase);

	int remainingSize = 1024 * 1024;

	while (stackStartPos < stackBase)
	{
		MEMORY_BASIC_INFORMATION mbi{};
		VirtualQuery((void*)stackStartPos, &mbi, sizeof(mbi));

		// 얻은 페이지의 속성이 얼마나 연속적인가를 나타낸다. (해당 크기만큼의 페이지는 동일한 속성을 가진다.)

		remainingSize -= mbi.RegionSize;

		if (remainingSize < 0)
			DebugBreak();

		printf("------------------------------------------------------------------------------\n");
		printf("		[TEST1 Count			: %d\n", Cnt++);
		printf("		[TEST1 Address			: 0x%p\n", (void*)stackStartPos);
		printf("		[TEST1 Page State		: %s\n", GetPageStateStr(mbi.State).c_str());
		printf("		[TEST1 Page Protection		: %s\n", GetPageProtectionConstantsStr(mbi.Protect).c_str());
		printf("		[TEST1 Region Size		: %s\n", GetKilloBytes(mbi.RegionSize).c_str());
		printf("		[TEST1 Remaining Size		: %s\n", GetKilloBytes(remainingSize).c_str());
		printf("\n\n");

		stackStartPos += mbi.RegionSize;

	}

}
	// 스택 오버플로 체킹을 위한 코드.
	// 32비트 기준
	// [스택의 마지막 RESERVE 부분]
	// [스택 GUARD]
	// [스택 GUARD]	<------┐
	//					   │
	// 상태에서      ------┘ 부분을 건드리면 마지막 RESERVE 부분이 건드려지면서 STACK OVERFLOW가 동작.
profile
낙서장

0개의 댓글