Coding Memo

메모리 할당 - Stomp Allocator 본문

Game Server (C++)

메모리 할당 - Stomp Allocator

minttea25 2022. 10. 6. 16:34

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 작성한 글입니다.


 

기존 Allocator에서 발생될 수 있는 문제 (에러조차 안뜰 수 있는 치명적인 문제들!)

 

첫 번째 문제

int main()
{
	vector<int> v{ 0, 1, 2, 3, 4 };
	for (int i = 0; i < 5; i++)
	{
		int value = v[i];
		if (value == 1)
		{
			v.clear();
		}
	}
}

value==1 일때 vector를 clear하고 v[i]로 접근하려고 하면 문제가 발생한다.

이 문제는 런타임에러가 바로 발생하여 바로 문제를 찾을 수 있다. (vector에 의해서)

 

 

두 번째 문제

class Unit
{
public:
	Unit()
	{
	}
	Unit(int hp) : _hp(hp)
	{
	}
	~Unit()
	{
	}

	int _hp = 10;
};

int main()
{
	Unit* unit = new Unit();
	delete unit;

	// 다른 코드로 인해 unit이 할당되었던 메모리가 다른 곳에서 사용되었다고 가정

	unit->_hp = 100; // ??
	cout << unit->_hp << endl;
}

이번 코드는 오류를 일으키지 않는다! (...)

 

당장 위 코드의 결과는 100이 출력이 되겠지만, 주석에서 언급했던 것처럼 unit이 할당되었던 메모리가 다른 곳에서 사용되었다면 _hp값은 제대로 나오지 않을 것이다.

free 된 이후에 해당 메모리를 다시 가져다 쓰려고 하면 치명적인 메모리 오염문제를 일으킬 수 있다.

(Use-After-Free 문제)

 

-> delete 한 후에 nullptr로 초기화 시켜주면 해결될까?

-> 해결되긴하지만 매번 그렇게 하기 매우 불편하기도 하고 여전히 unit에 그래도 접근은 가능한 상태이다.

 

세 번째 문제

캐스팅 했을 때 오버플로우와 비슷한 문제가 발생한다.

class PUnit
{
public:
	PUnit() {};
	virtual ~PUnit() {};
};

class Unit : public PUnit
{
public:
	Unit()
	{
	}
	Unit(int hp) : _hp(hp)
	{
	}
	~Unit()
	{
	}

	int _hp = 100;
};

int main()
{
	PUnit* p = new PUnit();
	Unit* u = static_cast<Unit*>(p);

	u->_hp = 10; // ?
}

PUnit의 크기보다 Unit의 크기가 더 크기 때문에 더 많은 메모리 사이즈를 가지고 있다. 따라서 캐스팅 시 Unit의 맴버 필드 값이 초기화 되지 않아 쓰레기 값을 가지고 있는 것을 확인 할 수 있다. (PUnit은 _hp에 대한 메모리가 없었으므로)

마찬가지로

즉, 해당 값을 다른 값으로 바로 초기화해주거나 적당한 값으로 할당한다면 문제가 되지 않을 수도 있지만, 이 값을 read 하려고 하면 큰 문제가 발생한다.

 

위 문제들의 발생을 막기위해 Stomp Allocator를 구현한다.

단, 릴리즈 버전이 아닌 디버그 모드에서만 작동하도록 한다.


page 단위로 할당을 할 것이다.

페이지 크기는 4KB로 잡자.

int main()
{
	SYSTEM_INFO info;
	::GetSystemInfo(&info);

	cout << info.dwPageSize << endl; // 4096 (4KB, 0x1000)
	cout << info.dwAllocationGranularity << endl; // 65536 (64KB, 0x10000)
}

 

VirtualAlloc과 VirtualFree을 사용한다.

https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc

 

VirtualAlloc function (memoryapi.h) - Win32 apps

Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. (VirtualAlloc)

learn.microsoft.com

 

VirtualAlloc으로 메모리를 할당할 때 사이즈나 읽기 전용, 쓰기 전용등의 여러가지 정책으로 구체적이게 할당할 수 있다.

VirtualFree도 마찬가지로 메모리 해제시에 여러가지 정책을 지정할 수 있다.

 

VirtualFree를 통해 메모리를 해제하게 되면, 코드 상 다시 그 메모리에 같은 변수로 접근하려고 하면 에러가 발생한다. (delete로 해제했을 때는 다시 그 메모리에 접근할 때 에러가 발생하지 않았다는 것을 기억해보자.)

int main()
{
	int* t = (int*)::VirtualAlloc(NULL, 4, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	::VirtualFree(t, 0, MEM_RELEASE);

	*t = 100; // error occurs!!!
}

BaseAllocator와 마찬가지로 Alloc 함수와 Release 함수를 선언하고 추가적으로 Page 크기를 정해두자.

class StompAllocator
{
	enum
	{
		PAGE_SIZE = 0x1000, // 4KB
	};
public:
	static void* Alloc(int size);
	static void Release(void* ptr);
};

 

 

 

 

위에서 언급된 3번째 문제(캐스팅에서의 메모리 오염 및 접근 문제)를 해결하기 위해, 실제 데이터가 배치되는 부분을 할당된 메모리의 끝에 둔다.

 

<<앞에다 실제 메모리를 둘 경우>>

캐스팅 후에, hp가 할당된 메모리 범위 내에 존재하고 있을 수 있기 때문에 값을 읽거나 써도 에러가 발생하지 않는다.

-> 나중에 큰 메모리 오염 문제를 일으킬 수 있다.

 

 

<<뒤에다 실제 메모리를 둘 경우>>

캐스팅 후에, hp가 할당된 메모리 밖에 존재하기 때문에 _hp에 접근하려고 하면 에러가 발생하게 된다!!!!!!!

 

 

 

byte 단위(__int8)로 할당하기 위해 캐스팅 한 후에 return 해주도록 하자.

 

 

Release도 메모리 위치를 바꿔주었으니 baseAddress를 계산하여 free 하도록 하자.

void* StompAllocator::Alloc(int32 size)
{
	const long long pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;
	const long long dataOffset = pageCount * PAGE_SIZE - size; // 메모리 뒤쪽부터 시작
	// 메모리의 시작 부분(데이터 사이즈)을 메모리 뒷쪽에 배치하기 (overflow 방지, but underflow 문제가 있긴함)
	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 long long address = reinterpret_cast<long long>(ptr);
	const long long baseAddress = address - (address % PAGE_SIZE);
	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}

 

마지막으로 debug 시에만 오류를 확인하기 위해 사용할 것이므로 다음과 같이 매크로에 추가한다.

 

#ifdef _DEBUG
#define xxalloc(size)		StompAllocator::Alloc(size)
#define xxrelease(ptr)		StompAllocator::Release(ptr)
#else
#define xxalloc(size)		BaseAllocator::Alloc(size)
#define xxrelease(ptr)		BaseAllocator::Release(ptr)
#endif // _DEBUG

대박! 이걸 어떻게 생각해낸거지........

'Game Server (C++)' 카테고리의 다른 글

Memory Pool  (0) 2022.10.09
메모리 할당 - STL Allocator  (0) 2022.10.09
메모리 할당 - Allocator  (0) 2022.10.06
Reference Counting  (0) 2022.09.30
Lock 구현 (Read-Write Lock)  (1) 2022.09.26