C++/C++

메모리 오염( Memory Stomping )방지를 위한 Allocator (작성 중)

Elan 2021. 10. 15. 01:41

목차

1. 메모리 오염이란?
  - 메모리 오염의 의미
  - 메모리 오염이 발생하는 상황 예시

2. Stomp Allocator
  - 사전 지식
    : 가상 메모리 ,가상 주소, 페이징
  - Stomp Allocator 개념

3. Memory Allocator 만들기
  - 메모리 할당/해제 연산자 오버로딩'
  - Customed Memory Allocator 만들어보기
    :  메모리 헤더 , 메모리 풀, Allocator, 

 

1.   메모리 오염( Memory Stomping )이란?

 

메모리 오염(손상)

 

주로 프로그램 상의 버그로 인해 예상치 못한 영역의 메모리가 오염되는 것을 말한다.

 

메모리 오염을 유발하는 코드가 존재할 경우 상당한 골칫거리가 된다.

 

그 이유는?

- 첫째, 잠재적으로 메모리 오염을 유발할 수 있는 코드를 작성하더라도,

당장 메모리 오염이 발생하지 않을 수도 있다.

 

- 둘째, 메모리 오염이 발생해도 눈치채기가 어렵다.

 

- 셋째, 메모리 오염으로 인한 버그가 발생해도 원이 되는 코드를 찾아내기 굉장히 어렵다.

 

 

메모리 오염 예시 코드 1

/* 	sizeof(Player) = 4  */
class Player {
public:
	Player() {}
	~Player() {}
	int hp = 0;
};
/* 	sizeof(Knight) = 16  */
class Knight : public Player {
public:
	Knight() {}
	~Knight() {}
	int mp = 0;
	int attack = 0;
	int gold = 0;
};

int main()
{
	Player* p1 = new Player();
	auto p = sizeof(Player);
	auto k = sizeof(Knight);
	Knight* knight = static_cast<Knight*>(p1);
	knight->mp = 10;
	knight->attack = 20;
	knight->gold = 100;
	/* 여기까지는 일단 크래시가 발생하지 않는다*/

	/* 여기서 부터는 크래시가 발생할 수도 있고 하지 않을 수도 있다 */
	std::cout << knight->mp << std::endl;
	std::cout << knight->attack << std::endl;
	std::cout << knight->gold << std::endl;
	return 0;
}

Player타입으로 생성된 인스턴스 p1을

다형성을 이용하여 Knight타입으로 다운 캐스팅하였다.

이미 여기서부터 잘 못된 동작이다.

심지어 할당되지 않은 메모리 영역에 접근 + 수정까지 시도하는데도 오류가 발생하지 않는다.

 

만약 static_cast대신 dynamic_cast를 사용하였다면

knight->mp에 접근하는 순간 <접근 위반> 예외를 던졌을 것이다.

 

결론 : 다운 캐스팅 시 주의하자.

 

 

 

메모리 오염 예시 코드 2 

 

아래 코드를 실행하면

벡터의 clear() 메서드가 호출된 뒤에 vector의 인덱스 operator[ ] 를 호출하는 순간 에러가 발생한다.

int main()
{
	std::vector<int> v{ 1,2,3,4,5,6,7,8 ,9,10 };
	for (size_t i = 0; i < 10; i++)
	{
		int value = v[i];// v.clear() 호출 후 vector의 operator[] 에서 에러 발생

		if (value == 3)
			v.clear();

		v[i] = 0;
	}
}

size를 초과하는 인덱스를 입력했기 때문에 인덱스 연산자 내부에서 예외를 던지는 것이다.

 

그래서 잘못된 동작이라는 것을 알 수 있다.

 

 

반면 Range-based for을 사용하면 높은 확률로 에러가 발생하지 않는다.

int main()
{
	std::vector<int> v{ 1,2,3,4,5,6,7,8 ,9,10 };
	for (auto& elem : v)
	{
		int value = elem;
		if (value == 3)
			v.clear();
		
		/* 컨테이너의 원소들을 clear한 직후에는
		더 이상 원소들에게 접근을 해서는 안되는데 계속해서 접근하고 있다.
		*/
		elem = 0; 
	}

	return 0;
}

그 이유는 std::vector의 clear() 메서드가 size는 0으로 만들지만 capacity는 건드리지 않기 때문에,

삭제 처리한 원소에 해당하는 메모리가 여전히 release되지 않은 상태로 존재한다.

그런데 Range-based for loop는 메모리로 바로 접근하는 방식이기 때문에

접근 및 수정이 가능하도록 동작해버린다.

 

이것은 정상적인 동작이 아니지만,

싱글 스레드 로직이라면 오류를 발생시킬만한 경우가 거의 없어서 문제를 알아차리기 어렵다.

 

이렇게 잠재적인 문제 내포하고 있는 코드가

서비스 단계로 넘어간 뒤 문제가 발생하게 된다면 난감한 상황이 생길 수 있으니 주의하자.

 

결론 : 반복문 내에서 컨테이너를 수정하는 경우 주의하자.


2. Stomp Allocator

 

 

 사전 지식 : 가상메모리, 가상 주소 & 페이징

 

제한된 양의 물리적 메모리(RAM)를 여러 프로세스에 할당해주는 것에는 한계가 있다.

운영체제는 이런 물리적 메모리 부족의 한계를 해결하기 위한 기술이 존재한다.


다음 상황을 가정해보자.

물리적 메모리(RAM) 용량은 8GB인데
프로세서에서 요구하는 메모리가 16GB이다.

명백히 물리적으로 제공할 수 있는 한계를 초과하는 상황이다.

이때 당장 필요한(자주 접근하는) 메모리 2GB를 제외한
나머지14GB의 데이터는
 보조기억장치(HDD)에 저장해두고,

현재 필요한 메모리(자주 사용되는 데이터)만 주기억장치(RAM)에 로드하여 사용하는 것이다.

이 기술을 Paging(페이징)이라 한다.

페이징(Paging)은 프로세스에서 요구하는 메모리를 할당 할때에 '가상메모리(Virtual Memory)'
'가상 주소(Virtual Address)'라는 것을 사용한다.

운영체제는 CPU의 도움(MMU, TLB)을 받아 페이징을 수행한다.
가상 주소로 가상 메모리의 위치를 식별/분류하며,
Paging기법을 이용하여 가상 메모리를 관리한다.


1) Windows API를 이용하여 페이징 직접 해보기


Windows에서는 프로세서에 대한 메모리 관리 관련된 정보를 확인할 수 있는

API를 아래와 같이 제공한다. ( GetSystemInfo )

[서적] 제프리 리처의 Windows via C++ ( Part.3 메모리 관리 참고 )

GetSystemInfo 함수로 시스템 정보 확인하기

페이지(Page)란 가상 메모리화 시킬 메모리 블럭 단위를 말하며, 프레임(Frame)이라 부르기도 한다.

<dwPageSize> - Page Size는 Page의 메모리 크기를 말하며 4096은 4096 bytes를 의미

Windows에서 프로세스에게 메모리를 할당해주는 기본 단위는 

64 KB(65536 bytes)이다. <dwAllocationGranularity>

그래서 페이지 할당을 요청하면 0x10000( 2^16 = 65536 )의 배수에 해당하는 메모리 주소가 반환될 것이다.



위의 내용은 다음에 알아볼 windows에서 제공하는 가상 메모리 API와 관련이 있기 때문이다.

바로 VirtualAllocVirtualFree함수이다.

이 API는 OS에게 가상 메모리를 예약/할당/해제를 요청한다.

OS는 프로세스 별로 할당해준 가상 메모리 영역을 기록하고 있고,

할당된 메모리 영역의 범위를 벗어난 접근을 할 경우 Memory Access Violation이 발생하게된다.

이것을 이용한 것이 바로 Stomp Allocator이다.

Memory Stomp Allocator Model

그림 출처 - Memory stomp allocator for Unreal Engine 4. | Pablo Zurita's blog (wordpress.com)

위의 그림은 Stomp Allocator가 가상 메모리를 어떻게 활용하는지를 보여주는 모델이다.

객체를 생성하기 위해 메모리는 1024 bytes가 필요해서
OS에게 가상 메모리를 요청하였고,
한 페이지(4096 bytes)를 할당받았다고 가정해보자.

할당된 메모리의 시작 주소가 0x0000'0000 라고 가정했을 때(실제로는 이 범위의 주소 공간은 사용할 수 없다.)
0x0000'0000 ~ 0x0000'03FF ( 0~1023 ) 까지만 사용하기 때문에
나머지 3072 bytes에 해당하는 0x0000'0400 ~  0x0000'0FFF( 1024 ~ 4095 )는
할당 받아놓고 사용하지 않고 허비되는 공간이된다.

이때 어플리케이션 레벨에서 원래 객체의 사이즈인 1024 bytes를 초과하는
영역 0x0000'0400~ 0x0000'0FFF사이의 메모리를 사용해버릴 경우 예상치 못한 결과를 초래할 수 있게된다.
이를 메모리 오염이라고 한다.

그런데!! 원래 의도한 1024 bytes를 초과한 메모리 공간인  0x0000'0400~ 0x0000'0FFF에 접근하여도
어플리케이션은 메모리를 오염시켰다는 사실을 알려주지 않는다.
때문에 개발자는 문제가 발생해도 원인을 파악하기 힘들다.

그런데 만약 객체를 생성하는데 사용하는 메모리 공간을 0x0000'0000 ~ 0x0000'03FF이 아니라
0x0000'0C00 ~ 0x0000'0FFF ( 3072 ~ 4095 )로 한다면 어떻게 될까?

0x0000'0C00 에서부터 1024bytes를 초과한 공간은 0x0000'1000 ~ 이상이 될 것이고
OS가 기록하고 있는 메모리 영역을 벗어난 접근을 하게되면
즉시 Memory Access Violation이라는 심각한 메모리 관련 보안 위반으로 보고
차단해버리게 된다.
그러면 어플리케이션은 크래쉬가 발생하게 되고
개발자는 어느 시점에 어느 부분에서 그것이 발생했는지 알 수 있게 된다.

이것이 Memory Stomp Allocator의 원리이자 전부이다.

Stomp Allocator 구현 코드

void* StompAllocator::AllocateMemory(int32 size)
{
	//메모리 할당 단위를 PAGE_SIZE로 하기 위한 과정
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;
	/* 할당한 메모리 중 마지막 메모리의 주소를 기준으로
		size만큼 앞으로 당긴 위치를 반환하기 위함
		
		예시) 4096 byte 할당 받았고, 메모리의 시작점이 0x0000'0000이라고 가정했을 때
		    사용할 메모리 크기가 16이라면
			 시작 주소(0000) + 할당받은 메모리 크기(4096) - 사용할 메모리 크기(16) = Offset(4080)
			 0x0000'0000 + 0x0000'1000 - 0x0000'0010 = 0x0000'0FF0 이 된다.

	BaseAddress(0000)                      Offset(4080)
			↓                                  ↓
			[            Allocated memory             ]
															 ↑
														  메모리 끝 주소(4096)
	*/
	const int64 offset = pageCount * PAGE_SIZE - size;
	
	// pageCount x PAGE_SIZE만큼 할당하고 할당 주소의 시작점을 받아옴
	/* VirtualAlloc 옵션
	- MEM_RESERVE : 메모리 할당 예약(페이징 파일에 실제 물리적 저장공간을 할당하지 않고, 프로세스의 가상 주소 공간 범위를 예약)
	 
	- MEM_COMMIT : 예약된 메모리 페이지에 MemoryManager Charge(할당한 메모리의 크기와 페이징 파일의 크기)를 할당. 가상 주소에 실제로 엑세스하지 않는 한 실제 물리적 페이지는 할당되지 않는다.
	
	※ 페이지를 예약하자 마자 사용할 것이라면?
	  MEM_COMMIT | MEM_RESERVE 옵션을 사용해야한다.
	  즉, 할당과 동시에 사용할 경우를 말한다.
	  ( 메모리 내용이 0으로 초기화 됨을 보장한다 )

	- PAGE_READWRITE : 읽기/쓰기 접근 허용, 데이터 실행방지 옵션이 활성화 된 경우 커밋된 영역에서 코드를 실행할 경우 액세스 위반 발생*/
	void* baseAddress = ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

	/* VirtualAlloc 함수를 통해 할당받은 메모리의 주소에서 실제 사용할 주소의 위치를 반환 	*/
	return static_cast<void*>(static_cast<int8*>(baseAddress) + offset);
}

void StompAllocator::ReleaseMemory(void* ptr)
{
	// AllocateMemory 함수에서 할당한 전체 메모리 중 실제 사용할 목적으로 반환받은 주소 ( baseAddress + offset )
	const int64 address = reinterpret_cast<int64>(ptr);

	// 위의 주소에서 offset을 뺀 주소 ( baseAddress + offset ) - offset
	// ex) base : 5000, offset : 500, address : 5500
	//		 5500 - ( 5500 % 4096 ) = 5500 - 1404  = 4096
	const int64 baseAddress = address - (address % PAGE_SIZE);
	// MEM_RELEASE옵션을 사용할 경우 VirtualAlloc함수를 통해 할당한 메모리의 base address를 인자로 넣어주어야한다
	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}

3.   Memory Allocator 만들기

 

위에서 만든 Stomp Allocator를 활용하기 위한

Customed Memory Allocator를 만들어보자.


 

메모리 할당/해제 관련 연산자 오버로딩



C++의 메모리 할당/해제 연산자도 오버로딩(재정의) 할 수 있다는 놀라운 사실 !!

아래 코드는 new, new[], delete, delete[] 연산자를 오버로딩한 예시이다.

// new operator overloading
void* operator new(size_t size)
{
	cout << "new! " << size << endl;
	void* ptr = ::malloc(size);
	return ptr;
}
// delete operator overloading
void operator delete(void* ptr)
{
	cout << "delete!" << endl;
	::free(ptr);
}
// new[] operator overloading
void* operator new[](size_t size)
{
	cout << "new[]! " << size << endl;
	void* ptr = ::malloc(size);
	return ptr;
}
// delete[] operator overloading
void operator delete[](void* ptr)
{
	cout << "delete![]" << endl;
	::free(ptr);
}

 

 

이제 메모리 할당/해제 연산자를 오버로딩하여 Customed Memory Allocator를 만들어 보자 !!

 

※ Tip
- 메모리 할당/해제 관련 연산자는 특별하게 취급하기 때문에
  클래스 내부에 정의해줄 경우
  static속성 여부에 관계없이 해당 클래스 전용 메모리 연산자로 취급한다.

더보기

아래 코드에서는 클래스 내부에
해당 클래스 전용 할당/해제 연산자를 정의해 놓은 것이다.

이러면 해당 클래스를 new/delete(할당/해제) 시 전용 메모리 할당/해제 연산자가 최우선순위로 호출된다.

class Knight
{
public:
	Knight()
	{
		cout << "Knight()" << endl;
	}

	Knight(int32 hp) : _hp(hp)
	{
		cout << "Knight(hp)" << endl;
	}

	~Knight()
	{
		cout << "~Knight()" << endl;
	}
	// static을 붙이지 않아도 똑같이 동작 함
	static void* operator new(size_t size)
	{
		cout << "Knight new! " << size << endl;
		void* ptr = ::malloc(size);
		return ptr;
	}
	// static을 붙이지 않아도 똑같이 동작 함
	static void operator delete(void* ptr)
	{
		cout << "Knight delete!" << endl;
		::free(ptr);
	}

	int32 _hp = 100;
	int32 _mp = 10;
};

// new operator overloading
void* operator new(size_t size)
{
	cout << "new! " << size << endl;
	void* ptr = ::malloc(size);
	return ptr;
}
// delete operator overloading
void operator delete(void* ptr)
{
	cout << "delete!" << endl;
	::free(ptr);
}
// new[] operator overloading
void* operator new[](size_t size)
{
	cout << "new[]! " << size << endl;
	void* ptr = ::malloc(size);
	return ptr;
}
// delete[] operator overloading
void operator delete[](void* ptr)
{
	cout << "delete![]" << endl;
	::free(ptr);
}

int main()
{	
	Knight* knight = new Knight(100);

	delete knight;
    
    
}
출력 결과

 

 

 

Customed Memory Allocator 만들기 1 단계

 

1) MemoryHeader 정의

enum
{
	SLIST_ALIGNMENT = 16
};

DECLSPEC_ALIGN(SLIST_ALIGNMENT)
struct MemoryHeader : public SLIST_ENTRY
{
	// 데이터를 위해 할당되는 메모리의 크기를 나타내는 헤더 역할로 데이터 앞에 위치 함
	// 메모리 정렬 구조 : [MemoryHeader][Data]
    
	MemoryHeader(int32 size) : allocSize(size) { }

	static void* AttachHeader(MemoryHeader* header, int32 size)
	{
		new(header) MemoryHeader(size); // placement new 를 사용한 생성자 호출
		return reinterpret_cast<void*>(++header);
	}

	static MemoryHeader* DetachHeader(void* ptr)
	{
		MemoryHeader* header = reinterpret_cast<MemoryHeader*>(ptr) - 1;
		return header;
	}

	int32 allocSize;
	// 기타 필요한 정보 추가
};
/*	SLISH_ENTRY는 16바이트 정렬 구조체이며
	다음 데이터의 포인터를 갖고 있음*/
typedef struct DECLSPEC_ALIGN(16) _SLIST_ENTRY {
    struct _SLIST_ENTRY *Next;
} SLIST_ENTRY, *PSLIST_ENTRY;

/*	SLIST_HEADER는 16바이트 정렬 공용체이며
	Singly-Linked List의 시작 노드 역할을 한다.
	Depth와 Sequence는 ABA문제를 해결하는데 사용.
    16바이트 정렬의 aligned_malloc을 사용하기 때문에 주소가 16의 배수이다.
    따라서 64비트 주소 중 하위 4비트 0이기 때문에
    그 4비트를 아껴서 내부적으로 사용하는 것이 Reserved이다.
    */
typedef union DECLSPEC_ALIGN(16) _SLIST_HEADER {
    struct {  // original struct
        ULONGLONG Alignment;
        ULONGLONG Region;
    } DUMMYSTRUCTNAME;
    struct {  // x64 16-byte header
        ULONGLONG Depth:16;
        ULONGLONG Sequence:48;
        ULONGLONG Reserved:4;
        ULONGLONG NextEntry:60; // last 4 bits are always 0's
    } HeaderX64;
} SLIST_HEADER, *PSLIST_HEADER;

 

2) MemoryPool 정의

DECLSPEC_ALIGN(16)
class MemoryPool
{
public:
	MemoryPool(int32 allocSize) : allocSize(allocSize)
	{
		::InitializeSListHead(&header);
	}
	~MemoryPool()
	{
		while (MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&header)))
			::_aligned_free(memory);
	}

	void	Push(MemoryHeader* ptr)
	{
		ptr->allocSize = 0;

		::InterlockedPushEntrySList(&header, static_cast<PSLIST_ENTRY>(ptr));

		usedCount.fetch_sub(1);
		reservedCount.fetch_add(1);
	}
    
	MemoryHeader* Pop()
	{
		/* InterlockedPopEntrySList 함수는
		_header에 가장 최근에 삽입된 데이터를 반환,
		만약 아무런 데이터도 들어있지 않을 경우 nullptr 반환*/
		MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&header));

		// 없으면 새로 만들다
		if (memory == nullptr)
		{
			// _aligned_malloc은 CPU 아키텍쳐에 최적화된 동작을 위해 메모리 주소를 16의 배수로 맞춰준다(특히 MS에서 제공하는 SLIST를 사용하려면 메모리 정렬을 16의 배수로 맞춰주어야한다)
			memory = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(allocSize, SLIST_ALIGNMENT));
		}
		else
		{
			ASSERT_CRASH(memory->allocSize == 0);
			reservedCount.fetch_sub(1);
		}

		usedCount.fetch_add(1);

		return memory;
	}

private:

	SLIST_HEADER	header;//메모리 풀 컨테이너( SLIST_HEADER는 MS사에서 만든 Lock-Free Stack의 시작 노드이다, 내부에서 사용되는 노드는 SLIST_ENTRY이다 )
	int32			allocSize = 0;// allocSize크기의 메모리를 풀링 한다
	atomic<int32>	usedCount = 0;//메모리 풀에서 꺼내어 사용 중인 객체의 갯수
	atomic<int32>	reservedCount = 0;// 메모리 풀에서 생성된 객체의 갯수
};

 

 

3) Allocator 정의

 

PoolAllocator

/*-------------------
	PoolAllocator
-------------------*/
// Memory*	GMemory = new Memory(); // GMemory는 Memory클래스 전역 변수

class PoolAllocator
{
public:
	static void* Alloc(int32 size);
	{
		return GMemory->Allocate(size); // GMemory는 Memory 클래스 타입의 전역 변수
	}
	static void		Release(void* ptr);
	{
		GMemory->Release(ptr);
	}
};


//메모리 풀을 이용한 new
template<typename Type, typename... Args>
Type* xNew(Args&&... args)
{
	Type* memory = static_cast<Type*>(PoolAllocator::Alloc(sizeof(Type)));
	new(memory)Type(forward<Args>(args)...); // placement new
	return memory;
}

template<typename Type>
void xDelete(Type* obj)
{
	obj->~Type();
	PoolAllocator::Release(obj);
}

 

MemoryPool 사용 예시

class Knight
{
public:
	int32 _hp = rand() % 1000;
};

SLIST_HEADER a;
SLIST_ENTRY b;

int main()
{
	for (int32 i = 0; i < 5; i++)
	{
		GThreadManager->Launch([]()
			{
				while (true)
				{
					Knight* knight = xNew<Knight>();

					cout << knight->_hp << endl;

					this_thread::sleep_for(10ms);

					xDelete(knight);
				}
			});
	}

	GThreadManager->Join();
}

 

 

 

Custom Memory Allocator 만들기 2 단계


4) Object Pool 정의

/* 
	※	Memory Pool과 Object Pool의 차이

	 * Memory Pool : 
	 - Memory Pool을 여러 Class가 공유하는 방식이며
	   메모리 오염이 발생했을 경우 원인을 찾기 어렵다.


	 * Object Pool : 
	 - Object Pool은 template class이며, static 맴버만 존재하기 때문에
	   각 class를 template 인자로 호출하는 경우에만 인스턴싱된다.
	   다시 말해서 Class별로 Pooling하는 방식이기 때문에 allocSize가 class에 따라 다르다. 
	   메모리 오염 발생 시 어떤 Class의 Object Pool에서 발생한 것인지 쉽게 파악할 수 있다.
*/

template<typename Type>
class ObjectPool
{
public:
	// 호출할 Type 생성자에 맞는 인자를 전달해주는 것 잊지 말기
	template<typename... Args>
	static Type* Pop(Args&&... args)
	{
#ifdef _STOMP_ALLOCATOR
		MemoryHeader* ptr = reinterpret_cast<MemoryHeader*>(
			StompAllocator::AllocateMemory(s_allocSize));

		Type* memory = static_cast<Type*>(
			MemoryHeader::AttachHeader(ptr, s_allocSize));
#else
		// 메모리풀에서 메모리를 꺼내올 때 메모리 크기를 같이 전달해 준다
		Type* memory = static_cast<Type*>(
			MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
#endif
		// Type 생성자에 맞는 인자를 전달
		new(memory)Type(std::forward<Args>(args)...); // placement new
		return memory;
	}
	// 사용이 끝난 객체를 ObjectPool에 반납
	static void Push(Type* obj)
	{
		obj->~Type();
#ifdef _STOMP_ALLOCATOR
		StompAllocator::ReleaseMemory(MemoryHeader::DetachHeader(obj));
#else
		s_pool.Push(MemoryHeader::DetachHeader(obj));
#endif
	}
	
	template<typename... Args>
	static std::shared_ptr<Type> MakeShared(Args&&... args)
	{
		std::shared_ptr<Type> ptr = { Pop(std::forward<Args>(args)...), Push };
		return ptr;
	}

private:
	/* template class는 static 속성이 붙었다고해서 전역으로 단 하나만 존재할 수 있는 것이 아니라
	 선언된 <Type>별로 각 하나씩 존재할 수 있다 */
	static int32		s_allocSize;
	static MemoryPool	s_pool;
};

template<typename Type>
int32 ObjectPool<Type>::s_allocSize = sizeof(Type) + sizeof(MemoryHeader);

template<typename Type>
MemoryPool ObjectPool<Type>::s_pool{ s_allocSize };


설명은 모두 주석으로 달아 놓았으니 참고.