Coding Memo
Use After Free 본문
Use After Free는 해제된 메모리를 더 이상 사용하지 않아야 되는 상황에서 사용하려고 할 때 나타나는 매우 큰 문제이다.
(Java나 C#같은 high-level 언어들은 이 문제에 신경을 덜 써도 된다.)
Use After Free 문제는 다음과 같은 상황에서 일어날 수 있다.
메모리를 delete(free)한 후에 그 영역에 접근하려고 할 때
즉, 객체의 관점에서 본다면, 객체를 delete(free) 했지만 객체에 대한 포인터가 그대로 남아있어 그 포인터에 접근하려고 할 때 일어난다.
다음 코드를 살펴보고 문제가 되는 점을 찾아보자.
using namespace std;
class A
{
public:
A()
{
cout << "constructor" << endl;
}
~A()
{
cout << "destructor" << endl;
}
public:
int value;
};
int main()
{
A* a = new A();
a->value = 10;
cout << a->value << endl;
delete a;
cout << a->value << endl; // Use After Free
}
a를 동적할당 후에 명시적으로 `delete a'를 통해 명시적으로 메모리를 해제한 후에 다시 a 포인터의 value 맴버에 접근하고 있다. 바로 이부분이 UAF 문제이다.
더 큰 문제는 따로 있다.
컴파일러가 이 문제를 잡아내지 못할 가능성이 있다는 것이다.
(실제로 문제가 없어서 컴파일이 정상적으로 실행되기도 한다.)
(IDE에서는 a에 마우스를 갖다 대면 `할당되지 않은 a에 접근하고 있습니다`라는 경고가 나올 것이다.)
실제로 위 코드를 실행 시켜보면 처음 a는 제대로 10이 출력되지만 메모리 할당 이후, a->value의 값에 대해서는 이상한 값이 출력되는 것을 알수 있을 것이다.
(만약, 어떤 게임에서 몬스터 처치 경험치가 10이었는데, 몬스터에 대한 메모리를 해제하고 다시 그 몬스터에 대한 경험치를 얻으려고하면 난리가 날 것이다!)
그렇다면 이 문제를 예방하고 해결할 수 있는 방법에는 무엇이 있을까?
1. delete 후에 포인터를 nullptr로 둔다.
솔직히 말해서 좀 애매한(?) 방법이기는 하다. delete까지 했는데 굳이 nullptr로 명시적으로 코드를 작성해야한다니...
int main()
{
A* a = new A();
a->value = 10;
cout << a->value << endl;
delete a;
a = nullptr;
cout << a->value << endl; // crash!! - read access is violation; a was nullptr
}
delete 후에 `a = nullptr` 코드를 명시적으로 작성하여 런타임 중에 이제 a는 nullptr니까 접근하지 말라고 알려준다.
위 코드를 실행하면 런타임 중에 에러가 나타날 것이다.
2. 스마트 포인터 사용
unique_ptr와 shared_ptr이라는 좋은 포인터가 있다!
shared_ptr로 선언된 객체는 (강한)참조가 더 이상 없으면 (=use_count가 0이 되고 약한 참조가 없으면) 메모리가 자동으로 해제된다. (자동으로 nullptr가 들어간다.)
아래 예시는 참조 수를 강제로 reset 시키고 다시 a에 접근하려고 시도했을 때의 코드이다. (오류가 나타난다.)
reset시에 use_count가 0이 되면서 소멸자까지 호출되는 것을 확인할 수 있다.
int main()
{
shared_ptr<A> a = make_shared<A>();
a->value = 10;
cout << a->value << endl;
cout << "Use count: " << a.use_count() << endl;
a.reset();
cout << "Use count after reset: " << a.use_count() << endl;
cout << a->value << endl; // crash!!; a was nullptr
}
unique_ptr로 선언된 객체는 해당 객체에 대한 포인터가 단 하나밖에 존재할 수 없다. 마찬가지로 이 포인터에 대해 메모리가 해제되거나 소유권이 이전되었을 때 해당 객체는 nullptr를 가리키게 된다.
int main()
unique_ptr<A> a = make_unique<A>();
a->value = 10;
cout << a->value << endl;
//A* b = a.release();
unique_ptr<A> b = std::move(a); // 소유권 이전
cout << a->value << endl; // crash!!; a was nullptr
}
Note: 소유권 이전이므로 소멸자는 호출 되지 않는다.
3. VirtualAlloc으로 메모리 플래그 지정
VirtualAlloc은 Windows에서 제공하는 함수로 가상 메모리를 할당하고 제어하는데 사용된다. 할당하면서 그 메모리 공간에 대한 특성(플래그)를 지정할 수 있다.
먼저, 짚고 넘어가야 할 내용이 있다.
VirtualAlloc은 요청한 size에 맞게 메모리를 할당하는 것이 아니라 granularity의 배수로 메모리 블록을 할당한다.
(예를 들어 granulairty가 4kb인데, 1kb사이즈의 크기를 할당 요청을 할 경우 실제 할당되는 크기는 4kb이다. 따라서 3kb에 대해서는 사용하지 않는 공간이 되므로, 메모리 파편화에 주의해야 한다.)
따라서 아래 예시 코드처럼 그대로 사용하면 안된다는 것을 기억하자. (예시를 위해서 짠 코드이다.)
int main()
{
LPVOID p = ::VirtualAlloc(NULL, sizeof(A), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 실제 할당되는 메모리 공간은 sizeof(A)가 아니다.
A* a = static_cast<A*>(p);
new(p) A();
a->value = 10;
cout << a->value << endl;
a->~A();
VirtualFree(p, NULL, MEM_RELEASE);
cout << a->value << endl; // Exception thrown: read access violation.
}
Note: 명시적으로 생성자와 호출자를 작성했다는 것을 확인하자.
Note2: `new(p) A()` 코드는 placement new로 p라는 메모리 공간에 A를 할당하는 코드이다.
결론적으로는 메모리 풀을 이용할 것이 아니라면, shared_ptr를 쓰는 것이 가장 좋을 수 있겠다. unique_ptr도 마찬가지로 상황에 맞게 사용하면 되겠다.
'Language > C++' 카테고리의 다른 글
[C++] 가상 소멸자 (0) | 2024.03.06 |
---|---|
[C++] pack pragma (메모리 정렬) (0) | 2024.03.05 |
값 범주 (value categories) (rvalue, lvalue, xvalue) (0) | 2023.10.27 |
Visual Studio glog 사용 (2) | 2023.10.26 |
메타 함수, 템플릿 1 (1) | 2023.10.26 |