서버/메모리 관리

4. Stomp Allocator

광란의슈가슈가룬 2024. 8. 16. 00:21

메모리가 이미 해제되었는데 사용하거나 사용할 수 있는 범위를 넘어서 사용하는 등 유효하지 않은 메모리 영역에 접근하는 것을 메모리 오염이라고 한다. 메모리 오염의 경우 크래시가 나지 않을 때도 있어 프로그램에 치명적일 수 있다. 예를 들어 허용된 범위를 넘어 메모리에 접근을 했는데 이미 다른 객체가 사용중인 메모리라던가 아니면 게임같은 경우 골드와 같이 유료재화를 담당하는 메모리라고 생각해보자. 이러한 상황을 예방하기 위한 것이 바로 Stomp Allocator이다.

 

Allocator.h

//-------------------
//  StompAllocator
//-------------------

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

};

Allocator.cpp

//-------------------
//  StompAllocator
//-------------------

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

	return ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
}

void StompAllocator::Release(void* ptr)
{
	::VirtualFree(ptr, 0, MEM_RELEASE);
}

VirtualAlloc은 메모리를 할당해주는 함수로 첫 번째 인자는 할당할 메모리의 주소를 넣는데 NULL을 사용할 경우 알아서 할당해준다. 두 번째는 할당할 메모리의 크기이다. 세 번째 인자는 메모리를 어떻게 사용할지에 대한 옵션이다. 할당과 동시에 사용할 수도 있고 나중에 사용할 것이다라고 예약을 할 수도 있다. 마지막 네 번째 인자는 보안정책으로 읽기만 가능한지 읽고 쓰기 모두 가능한지 등을 설정할 수 있다. 

VirtualFree는 메모리를 해제하는 함수로 메모리의 시작주소, 해제할 메모리의 크기, 옵션을 인자로 받는다. 전체를 해제할 경우 0을 넣으면 된다. 위에서는 메모리를 완전히 해제할 것이므로 MEM_RELEASE를 옵션으로 사용했다.

 

CoreMacro.h

//------------------
//	Memory
//------------------

#ifdef _DEBUG
#define Xalloc(size)		StompAllocator::Alloc(size)
#define Xrelease(ptr)		StompAllocator::Release(ptr)
#else
#define Xalloc(size)		BaseAllocator::Alloc(size)
#define Xrelease(ptr)		BaseAllocator::Release(ptr)
#endif

디버그 단계의 매크로도 StompAllocator로 바꿔주자

 

delete로 메모리를 해제해도 포인터는 주소를 참조(dangling pointer)하고 있기 때문에 nullptr로 밀어주어야 한다. StompAllocator는 애초에 해제된 메모리에 접근을 시도하면 크래시가 나기 때문에 디버그 단계에서 오류를 더 쉽게 잡을 수 있다.

 

하지만 위의 코드도 메모리 오버플로우를 잡아주지는 못한다.

VirtualAlloc 특성상 페이지 단위(4KB)로 메모리를 할당해주기 때문에 만약 객체가 8바이트 정도만 사용한다 하더라도 4KB라는 큰 메모리를 할당받게 되는 것이다. 그렇게 되면 8바이트를 넘어선 범위에 접근을 하더라도 하드웨어 입장에서는 할당한 메모리를 사용하는 것이기 때문에 문제가 없다고 판단한다.

 

해결방안은 의외로 단순하다. 메모리를 할당할 때 처음부터 사용하는 것이 아니라 끝에서부터 사용하는 것이다. 

1)
[[사용할 메모리]					  	      ]
2)
[						[사용할 메모리]]

이런식으로 끝에서부터 사용하면 오버플로우는 방지할 수 있다. 하지만 당연히 언더플로우는 감지할 수 없는데 생각해보면 대부분의 버그나 문제들은 오버플로우에서 일어난다. 그렇기 때문에 이런식으로 오버플로우를 방지하는 것이 훨씬 낫다.

 

Allocator.cpp

//-------------------
//  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);
}

코드만 한 두줄 추가하여 간단하게 오버플로우까지 탐지할 수 있도록 수정하였다. 간단한 수학 공식이므로 따로 설명을 적지는 않았다.

'서버 > 메모리 관리' 카테고리의 다른 글

6. Memory Pool #1  (0) 2024.08.20
5. STL Allocator  (0) 2024.08.16
3. Allocator  (0) 2024.08.11
2. 스마트포인터  (0) 2024.08.11
1. Reference Counting  (0) 2024.08.09