상세 컨텐츠

본문 제목

Stomp allocator

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 9. 11. 23:39

본문

c++ 기본 new와 delete, malloc, free를 쓰면 해제된 메모리에 접근했는데도 crash가 발생하지 않는 문제가 있다.

엉뚱한 메모리를 오염시키고 결국 오류가 나는 곳은 문제의 원인과 멀리 떨어져있는 경우가 많아서 디버그 하기도 쉽지않다.

// Case 1
Knight* k1 = new Knight();
k1->_hp = 200;
k1->_mp = 50;
delete k1;
k1->_hp = 100; // User-After-Free

// Case 2
vector<int32> v{ 1,2,3,4,5 };
for (int32 i = 0; i < 5; i++)
{
	int32 value = v[i];
	// TODO
	if (value == 3)
	{
		v.clear(); // 이후부터 유효하지 않은데, 또 접근.. 메모리 오염
	}
}

// Case 3
Player* p = new Player();
Knight* k = static_cast<Knight*>(p); // casting 오류 ,, dynamic_cast를 사용하고 nullptr를 체크하면 되지만 느림.
k->_hp = 100; // 메모리 오염

 

OS는 가상메모리를 사용한다. 프로그램에서 메모리를 요구하면 실제 메모리를 할당한 후, 물리 주소를 반환하는 게 아니라 물리주소에 매핑된 가상메모리를 반환한다. 따라서 다른 프로그램에서 같은 메모리를 주소를 참조한다고 하여도 실제 물리 주소는 다르다. 

 

운영체제는 메모리 할당 최소단위가 있어 아무리 잘게 메모리 할당을 요청해도 최소단위 만큼 메모리를 할당한다. 최소단위는 페이자 불리는 동일한 크기의 여러 구역으로 나뉜다. 이러한 정책을 페이징이라고 한다. 각 페이지마다 보안정책을 설정할 수 있다.

 

// 2GB [                      ]
// 2GB [ooxxooxxoxooooxoxoooos]
// 페이징 정책을 쓴다 4Kb[r][w][rw][][][][][] 페이지에 보안정책을 설정할 수 있다.

SYSTEM_INFO info;
::GetSystemInfo(&info);

info.dwPageSize; // 4KB (0x1000)
info.dwAllocationGranularity; // 64KB (0x10000) 메모리 할당할 때 이 숫자의 배수로 할당한다.

 

운영체제에 직접 메모리 할당, 반환을 요청하는 API가 있는데 이를 이용하면 잘못된 주소를 참조할 때 바로 crash가 발생한다.

 

윈도우에서 할당 요청 함수는 VirtualAlloc, 반환 함수는 VirtualFree이다. 이를 이용해 StompAllocator를 만들어보자.

Allocator.h, Allocator.cpp

/*---------------------
	StompAllocator
---------------------*/

class StompAllocator
{
	enum { PAGE_SIZE = 0x1000 };

public:
	static void* Alloc(int32 size);
	static void Release(void* ptr);
};
/*---------------------
	StompAllocator
---------------------*/

void* StompAllocator::Alloc(int32 size)
{
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;
	const int64 dataOffset = pageCount * PAGE_SIZE - size;

	void* baseAddress = ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	return static_cast<void*>(static_cast<int8*>(baseAddress) + dataOffset);
}

void StompAllocator::Release(void* ptr)
{
	const int64 address = reinterpret_cast<int64>(ptr);
	const int64 baseAddress = address - (address % PAGE_SIZE);
	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}

pageCount는 가장 작은 PAGE_SIZE의 배수를 찾는다. pageCount * PAGE_SIZE가 요청할 메모리 크기이다.

dataOffset은 메모리 overflow를 찾기 위해 필요하다. offset없이 할당하면

[[       ]                 ] 와 같이 할당된다. 실제 사용메모리 부분을 할당 메모리 끝쪽에 위치시키면 overflow가 났을 때 잡아준다. [                [       ]]

 

메모리 반환하기 위해서 다시 원래 할당 주소를 찾아야 한다. address % PAGE_SIZE으로 offset을 구하고 그만큼 앞으로 이동한 포인터를 해제한다. 

 

메모리 alloc release 매크로를 바꾸면 모든 코드에 적용되어 편리하게 사용할 수 있다.

CoreMacro.h

	/*----------------------
			Memory
	----------------------*/
#ifdef _DEBUG
	#define x_alloc(size) StompAllocator::Alloc(size)
	#define x_release(ptr) StompAllocator::Release(ptr)
#else
	#define x_alloc(size) BaseAllocator::Alloc(size)
	#define x_release(ptr) BaseAllocator::Release(ptr)
#endif
// overflow 문제 잡기
//[                         [    ]]
Knight* knight = (Knight*)xnew<Player>();

knight->_hp = 100;
xdelete(knight);

User-After-Free 뿐 아니라 메모리 overflow 상황도 대응가능하다.

 

1. 페이징 정책이란? 4kb 페이지 사이즈가 의미하는 것은 무엇?

- 운영체제가 사용하는 메모리 관리의 기본 단위.

2. Alloc 함수에서 dataoffset 구하는 공식의 의미는?

- 데이터를 할당한 메모리 끝에 위치시켜 overflow를 찾게 하기 위함.

3. 2와 같이 하면 overflow를 찾을 수 있는 이유는?

- 페이징 정책 사이즈보다 높은 메모리 주소에 접근하면 overflow가 발생하기 때문.

4. stomp allocator 가 user after free 문제를 해결하는 방법은?

- 운영체제에 실제 메모리 해제를 요청하기 때문에 exception 발생함.

- 언어 표준은 실제 메모리를 요청 즉시 해제 할 수도 안 할 수도 있음.

 

참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

Memory pool  (0) 2021.09.15
STL allocator  (0) 2021.09.12
Allocator  (0) 2021.09.11
스마트 포인터  (0) 2021.09.10
Reference Counting  (0) 2021.09.08

관련글 더보기

댓글 영역