C++ Server - 멀티스레드 프로그래밍 (6)

창고지기·2025년 9월 18일
0

Cpp Server

목록 보기
6/6
post-thumbnail

Allocator

배경

  • new/delete는 문맥전환의 확률이 있음
  • new/delete는 메모리 단편화의 가능성이 있음
  • 한번에 많은 양을 할당 받고 거기서 잘 쪼개서 사용하자!

new/delete override

  • new/delete 또한 오버라이딩의 대상
    • 전역으로 오버라이드 혹은 원하는 클래스 내부에서 오버라이드 가능
class Knight
{
public:
	Knight()
	{
		cout << "Knight()" << endl;
	}

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

	~Knight()
	{
		cout << "~Knight()" << endl;
	}

	// new / delete overriding (local)
	// static 붙은거 처럼 동작
	/*void* operator new(size_t size)
	{
		cout << "Knight new! " << size << endl;
		void* ptr = ::malloc(size);
		return ptr;
	}

	void operator delete(void* ptr)
	{
		cout << "Knight delete!" << endl;
		::free(ptr);
	}*/

	int32 _hp = 100;
	int32 _mp = 50;


};

// new / delete overriding (Global)
void* operator new(size_t size)
{
	cout << "new!" << size << endl;
	void* ptr = ::malloc(size);
	return ptr;
}

void operator delete(void* ptr)
{
	cout << "delete!" <<endl;
	::free(ptr);
}

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

void operator delete[](void* ptr)
{
	cout << "delete![]" << endl;
	::free(ptr);
}

int main()
{
	// 메모리 할당 후 생성자 호출
	Knight* kinght = new Knight();
	delete kinght;
}


사용자 정의 할당/소멸 (Allocator)

  • new/delete 기능에 원하는 기능을 넣을 수 있음
  • placement new
    • 이미 할당된 메모리에 대해서 생성자를 호출
    • new(memory)Type(std::forward<Args>(args)...);
    • 이미 할당된 메모리 memory 위치에서 Type클래스의 생성자 Type(args)를 호출해줘
//Allocator.h
class BaseAllocator
{
public:
	static void*	Alloc(int32 size);
	static void		Release(void* ptr);
};
//Allocator.cpp
#include "Allocator.h"

void* BaseAllocator::Alloc(int32 size)
{
	return ::malloc(size);
}

void BaseAllocator::Release(void* ptr)
{
	::free(ptr);
}
//Memory.h
#include "Allocator.h"

template <typename Type, typename... Args>
Type* xnew(Args&&... args) // 보편참조
{
	Type* memory = static_cast<Type*>(BaseAllocator::Alloc(sizeof(Type)));

	// placement new
	// 이미 할당된 메모리 위에 생성자를 호출해줘
	// forward로 왼값이면 왼값으로, 오른값이면 오른값으로
	new(memory)Type(std::forward<Args>(args)...);
	return memory;
}

template <typename Type>
void xdelete(Type* obj)
{
	obj->~Type();
	BaseAllocator::Release(obj);
}
int main()
{
	// 메모리 할당 후 생성자 호출
	Knight* kinght = new Knight();
    // 소멸자 호출 후 메모리 해제
	delete kinght;

	Knight* k2 = xnew<Knight>();
	xdelete(k2);

	Knight* k3 = xnew<Knight>(100);
	xdelete(k3);

	int a = 0;
}


Allocator for Debug

배경

  • 기존의 포인터를 사용하면 메모리 오염 문제 발생
    • Use-After-Free
    • 캐스팅에 의한 오염 (주로 오버플로우)
  • 이를 개발 단계에서 찾아내기 위해 Guard Pages Allocator 사용
    • 스마트 포인터를 사용하면 Use-After-Free는 거의 해결
    • Unreal 에서는 Stomp Allocator라 부르는 듯

개념

가상 메모리, 페이징

Guard Pages Allocator

  • 페이지의 블록 단위로 메모리 할당을 받지 말고 페이지 통으로 받자
  • 페이지의 앞 뒤로 보호 페이지를 넣어서 다른 페이지에 접근하지 못하도록하자.
    • 보호 페이지에 접근시 Crash
  • 페이지를 통으로 받으니까 반납도 한 번에 이루어짐
    • 여기서 Use-After-Free 잡힘
  • User 페이지 내부에서의 언더/오버 플로우에는 취약

구현

// 위의 이론을 기반으로 만든 코드
// 최소3개의 페이지 할당
/*----------------------
	  GuardAllocator
-----------------------*/

void* GuardAllocator::Alloc(int32 size)
{
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;
	// 앞 뒤로 페이지를 넣기때문에 2개 더함.
	const int64 totalPage = pageCount + 2;

	// 전체 페이지 할당
	void* baseAddress = ::VirtualAlloc(
		NULL,
		totalPage * PAGE_SIZE,
		MEM_RESERVE | MEM_COMMIT,
		PAGE_READWRITE
	);

	if (!baseAddress) return nullptr;

	DWORD oldProtect;

	// 앞의 페이지를 가드로 변환
	::VirtualProtect(
		baseAddress,
		PAGE_SIZE,
		PAGE_NOACCESS,
		&oldProtect
	);

	// 맨 뒤의 페이지를 가드로 변환
	::VirtualProtect(
		static_cast<int8_t*>(baseAddress) + ((totalPage - 1) * PAGE_SIZE),
		PAGE_SIZE,
		PAGE_NOACCESS,
		&oldProtect
	);
    
	// 오버 플로우 감지를 위해서 사용할 메모리를 페이지의 뒷부분에 붙힘
	//[guard][              [사용공간]][guard]
	const int64 dataOffset = PAGE_SIZE + (pageCount * PAGE_SIZE) - size;
	return static_cast<void*>(static_cast<int8_t*>(baseAddress)  + dataOffset);

}

void GuardAllocator::Release(void* ptr)
{
	if (!ptr) return;

	// User Memory가 포함된 전체 블록의 base 주소 계산
	const int64 address = reinterpret_cast<int64>(ptr);
	const int64 baseAddress = address - (address % PAGE_SIZE) - PAGE_SIZE;

	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}
  • 많은 메모리 사용
  • 상대적으로 엄격한 버그 탐지

// 간단한 코드 (덜 엄격한 버전)
// 혹시라도 연속된 가상메모리가 잡히면 못 잡을 수 있음
// 실제로 위의 경우는 잘 없는듯?
/*----------------------
	  StopmpAllocator
-----------------------*/

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
	);
	// 계산을 바이트 단위로 할 수 있도록 8바이트 포인터로 변환
	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);
}
  • 적은 메모리 사용
  • 상대적으로 덜 엄격한 탐지 (대부분은 경우는 탐지 가능하다고...)
profile
일단 창고에 넣어놓으면 언젠가는 쓰겠지

0개의 댓글