
class A
{
public:
int hp = 0;
};
int main()
{
A* a = new A();
delete a;
a->hp = 200;
}
위 코드를 보자 분명 delete를 하고 a = nullptr로 밀어준다음에 nullptr확인을 안하고 해제된 메모리에 값을 쓰고 있다.
분명히 잘못된 코드인데 때에 따라서 크래쉬가 날 수도 있고 안 날 수도 있다.
delete키워드가 소멸자를 호출해주고 메모리를 해제하는 것은 맞지만 메모리 해제를 바로 os에게 요청하지 않기때문이다.
delete는 객체가 차지했던 메모리 블록을 힙에 반환한다. 여기서 중요한 부분은 메모리 블록을 운영체제에게 즉시 반환하는게 아니라 힙 관리에게 반환한다는 것이다.
힙 관리자는 반환된 메모리 블록을 재사용가능한 공간으로 관리한다. 따라서 해당 메모리 영역은 다른 동적할당 요청에 의해 다시 사용될 수 있다.
stomp allocator
따라서 개발단계에서 메모리 누수나 use after free와 같은 문제들을 해결 및 보완할 수 있는 'stomp allocator'가 무엇인지 알아보고자 한다.
stomp할당자는 메모리 오염 문제를 해결하기 위한 메모리 할당 방식이다.
stomp allocator는
- 페이지 단위 할당 : 메모리를 페이지 단위로 할당하여, 할당된 메모리 영역을 명확하게 구분 한다.
- 메모리 보호 : 할당된 메모리 영역에 대한 접근 권한을 관리하여 잘못된 접근을 방지한다.
- 메모리 해제 감지 : 해제된 메모리 영역에 대한 접근 감지하여, 메모리 오염 오류를 발견한다.
이것을 구현하려면 기존의 new, delete등으로 객체를 할당/해제를 하는게 아니라 os에게 직빵으로 요청하는 함수를 사용하면된다. VirtualAlloc, VirtualFree를 통해 이게 가능하다.
VirtualAlloc 매개 변수는 아래와 같다.
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, // 기본 NULL
[in] SIZE_T dwSize, // 사용하고 싶은 데이터 크기
[in] DWORD flAllocationType, // MEM_COMMIT | MEMRESERVE
[in] DWORD flProtect // 메모리 관리 정책 설정 READ WRITE 등 PAGE_READWRITE
);
구현은 아래와 같다.
(PAGE_SIZE는 0x1000 == 4096 = 4kb 페이지 크기 단위이다)
void* StompAllocator::Alloc(int32 size)
{
// (size : 4, 4096 - 1) / 4096
const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;
// [ (dataOffset)[ ]]
// 1 * 4096 - size(4)
const int64 dataOffset = pageCount * PAGE_SIZE - size;
// 시작 주소 baseAddress에 반환후 + dataOffset만큼 더해 실제 데이터가 있는 가상 메모리 주소 반환
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);
}
Alloc함수부터 설명하도록 하겠다. 우선 4바이트 int 타입을 동적할당 한다고 했을 때, 아래와 같이 사용할 수 있다.
int* a = (int*)StompAllocator::Alloc(4);
이때 VirtualAlloc을 통해서 os에게 직접 메모리 공간 할당 요청을 하게된다.
내가 4바이트만 요청하더라도 각각의 프로세스들은 가상 메모리 공간을 사용하기 때문에 페이지 크기의 최소 단위인 4kb를 할당해준다.
Alloc함수에서 pageCount와 dataOffset을 구하는 이유는 overflow를 탐지하기 위해서이다.
메모리 할당 정책 자체를 [ [실제 데이터]] 이런식으로 가장 끝에 두어 페이지를 넘어가면 바로 크래쉬가 날 수 있게 유도하는 것이다.
이유는 메모리 할당시 페이지 단위로 할당한다. 만약 데이터를 앞쪽에 배치한다면 활살표로 가르킨 부분에 대해서 overflow가 발생할 확률이 높다.
시스템 상으로 사용할 수 있지만 접근하면 안되는 영역을 접근하는 것이다.

그래서 실제 데이터를 앞이 아니라 사이즈 계산을 통해 가장 뒤에 배치하여 페이지 단위를 넘기는 상황을 미연에 방지하는것이다.

이제 실제 VirtualAlloc을 보자.
void* baseAddress = ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
NULL은 기본 상태, 할당 받을 페이지 크기, MEM_RESERVE | MEM_COMMIT을 통해 즉시 할당받고 시작 주소를 baseAddress에 반환 받는다.
Alloc함수에서 실제 데이터가 있는 위치를 반환하기 위해 baseAddress + dataOffset으로 정확하게 실제 데이터가 있는 주소를 반환할 수 있다.
Release함수를 이제 보도록 하자.
위에서 동작할당한 a를 해제할 것이다.
int* a = (int*)StompAllocator::Alloc(4);
StompAllocator::Release(a);
a는 int64형태의 주소로 형변환하고 실제 페이지 시작 주소를 찾아야한다.
const int64 address = reinterpret_cast<int64>(ptr);
const int64 baseAddress = address - (address % PAGE_SIZE);
위처럼 구할 수 있는데 예를 들어보도록 하자.
메모리는 페이지 단위로 할당된다고 했다. 1번 페이지 : 0x1000~0x1FFF, 2번 페이지 : 0x2000~0x2FFF가 될 것이다.
a가 2번 페이지에 0x2FFC에 있다고 치자. 여기에 % PAGE_SIZE(0X1000)을 하게 되면 FFC가 뽑히게 된다.
0x2FFF - FFC를 하면 0x2000이 나오게 되고 이는 a가 있는 페이지 시작 주소 위치이다.
VirtualFree의 매개 변수는 해제할 메모리 영역 주소, 크기, 정책인데
첫번째에는 말 그대로 해제할 메모리 영역의 주소를 넘기고 두번째로 0을 넘기는 경우에는 MEM_RELEASE인 경우에만 가능하다. 3번째는 MEM_RELEASE를 보통 넣는다.(해제하는경우)
다른 추가 옵션은 https://learn.microsoft.com/ko-kr/windows/win32/api/memoryapi/nf-memoryapi-virtualfree 여기를 보도록 하자.
이렇게 stomp allocator를 사용하면 개발단계에서 use after free, 댕글링 포인터 등의 문제들을 미연에 방지할 수 있지만 알아야할 지식이 필요해보인다. 언리얼에서도 stomp allocator를 사용한다고 하니 공부를 해두자.
정리
- 각각의 프로세스들은 자신만의 고유한 메모리 영역을 가진다. 이때 실제 물리 메모리 주소를 사용하지 않고 가상 메모리 주소를 사용한다.
- 가상 메모리는 실제 물리 메모리 보다 큰 메모리 공간을 사용할 수 있게하는 기법이다. 이 기법을 통해 실제 용량보다 큰 프로그램들을 메모리에 적재할 수 있다.
- 가상 메모리는 운영체제에 의해 페이지 단위로 메모리 공간이 관리되는데(이때 페이지 기본 단위는 4KB) 동적할당을 하는 경우 4바이트를 요청해도 4kb가 할당되는 이유가 그 이유이다.
- 그럼 4바이트만 사용한다고해도 시스템상 4바이트 이상 사용이 가능하긴 하다. 하지만 overflow문제가 있기 때문에 ::VirtualAlloc을 통해 StompAlloc을 구현하고 실제 데이터의 위치를 메모리 공간 끝에 할당하여 overflow를 방지하는 합리적인 방법을 사용한다.
- new, delete의 경우 OS에게 바로 메모리 해제 요청을 하지 않기 때문에 StompAllocator를 통해 메모리를 할당 및 해제를 하여 바로 크래쉬를 내는 방법으로 use after free와 같은 문제를 보완할 수 있다.
'CPP' 카테고리의 다른 글
[C++] 다형성과 가상 소멸자 (0) | 2025.03.25 |
---|---|
[C++] 스마트 포인터 (0) | 2023.09.14 |
[C++] union (DX Matrix) (0) | 2023.09.04 |
[C++] initializer_list (0) | 2023.08.23 |
[C++] 가변 길이 템플릿과 후행 리턴 타입 (0) | 2023.08.20 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!