메모리 누수 탐지
메모리 누수의 이해
메모리 누수는 프로그램의 성능을 저하시키고 잠재적으로 시스템 불안정을 야기할 수 있는 심각한 문제입니다.
이 장에서는 C++ 프로그램에서 메모리 누수를 탐지하고 방지하는 다양한 기법과 도구를 심도 있게 살펴보겠습니다.
메모리 누수는 프로그램이 더 이상 사용하지 않는 메모리를 해제하지 않을 때 발생합니다.
이로 인해 프로그램이 점점 더 많은 메모리를 소비하게 되어 결국 시스템 자원을 고갈시킬 수 있습니다.
메모리 누수의 유형
- 단순 누수 : 할당된 메모리를 해제하지 않는 경우
- 순환 참조 누수 : 객체들이 서로를 참조하여 해제되지 않는 경우
- 캐시 누수 : 캐시된 객체를 적절히 관리하지 않는 경우
- 콜백 누수 : 등록된 콜백을 제거하지 않는 경우
메모리 누수 예제
void memory_leak_example() {
int* array = new int[100]; // 메모리 할당
// array를 사용
// delete[] array; 를 호출하지 않고 함수 종료
} // 메모리 누수 발생
메모리 누수 탐지 도구
Valgrind
Valgrind는 강력한 메모리 디버깅 도구입니다.
사용 방법
valgrind --leak-check=full --show-leak-kinds=all ./your_program
#include <iostream>
int main() {
int* p = new int(42);
std::cout << *p << std::endl;
// delete p; // 주석 처리하여 메모리 누수 발생
return 0;
}
Valgrind 출력 분석
==12345== HEAP SUMMARY:
==12345== in use at exit: 4 bytes in 1 blocks
==12345== total heap usage: 2 allocs, 1 frees, 72 bytes allocated
==12345==
==12345== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x108698: main (in /path/to/your_program)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 4 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
AddressSanitizer
AddressSanitizer는 컴파일 시간과 런타임에 메모리 오류를 탐지합니다.
사용 방법
g++ -fsanitize=address -g your_program.cpp -o your_program
./your_program
#include <iostream>
int main() {
int* array = new int[100];
array[100] = 0; // 버퍼 오버런
delete[] array;
return 0;
}
AddressSanitizer 출력 분석
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x614000000190 at pc 0x55555555483d bp 0x7fffffffe350 sp 0x7fffffffe340
WRITE of size 4 at 0x614000000190 thread T0
#0 0x55555555483c in main (/path/to/your_program+0x83c)
#1 0x7ffff7a62bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
#2 0x555555554729 in _start (/path/to/your_program+0x729)
0x614000000190 is located 0 bytes to the right of 400-byte region [0x614000000040,0x614000000190)
allocated by thread T0 here:
#0 0x7ffff7afe517 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0517)
#1 0x555555554801 in main (/path/to/your_program+0x801)
#2 0x7ffff7a62bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/path/to/your_program+0x83c) in main
Shadow bytes around the buggy address:
0x0c287fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff8000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0c287fff8010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c287fff8020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c287fff8030: 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c287fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==12345==ABORTING
스마트 포인터를 활용한 메모리 누수 방지
스마트 포인터는 자동으로 메모리를 관리하여 많은 종류의 메모리 누수를 방지할 수 있습니다.
std::unique_ptr
std::unique_ptr
는 단일 소유권 모델을 제공합니다.
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
void use() { std::cout << "Resource used\n"; }
};
void use_resource() {
std::unique_ptr<Resource> res = std::make_unique<Resource>();
res->use();
// res가 스코프를 벗어날 때 자동으로 메모리 해제
}
int main() {
use_resource();
return 0;
}
std::shared_ptr
std::shared_ptr
는 여러 포인터가 하나의 객체를 공유할 때 유용합니다.
#include <memory>
#include <iostream>
#include <vector>
class Resource {
public:
Resource(int id) : id_(id) { std::cout << "Resource " << id_ << " acquired\n"; }
~Resource() { std::cout << "Resource " << id_ << " released\n"; }
void use() { std::cout << "Resource " << id_ << " used\n"; }
private:
int id_;
};
int main() {
std::vector<std::shared_ptr<Resource>> resources;
for (int i = 0; i < 3; ++i) {
resources.push_back(std::make_shared<Resource>(i));
}
for (const auto& res : resources) {
res->use();
}
// resources가 스코프를 벗어날 때 모든 Resource 객체가 자동으로 해제됨
return 0;
}
메모리 누수 디버깅 실습
다음 코드에 있는 메모리 누수를 찾아 수정해보세요.
#include <iostream>
#include <vector>
class ComplicatedObject {
public:
ComplicatedObject() {
data_ = new int[1000];
std::cout << "ComplicatedObject created\n";
}
~ComplicatedObject() {
std::cout << "ComplicatedObject destroyed\n";
// delete[] data_; // 주석 처리하여 메모리 누수 발생
}
private:
int* data_;
};
void potential_leak() {
ComplicatedObject* obj = new ComplicatedObject();
// 예외가 발생하면 obj가 해제되지 않음
throw std::runtime_error("Something went wrong");
delete obj;
}
std::vector<ComplicatedObject*> global_objects;
void add_to_global() {
global_objects.push_back(new ComplicatedObject());
}
int main() {
try {
potential_leak();
} catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
for (int i = 0; i < 5; ++i) {
add_to_global();
}
// global_objects의 내용을 해제하지 않고 프로그램 종료
return 0;
}
이 코드를 컴파일하고 Valgrind나 AddressSanitizer를 사용하여 메모리 누수를 탐지해보세요. 그리고 스마트 포인터와 RAII 원칙을 사용하여 코드를 개선해보세요.
고급 메모리 프로파일링 기법
Massif (Valgrind의 힙 프로파일러)
Massif를 사용하여 프로그램의 메모리 사용량을 시각화할 수 있습니다.
valgrind --tool=massif ./your_program
ms_print massif.out.<pid>
heaptrack
heaptrack은 동적 메모리 할당을 추적하고 분석하는 도구입니다.
heaptrack ./your_program
heaptrack_gui heaptrack.your_program.<pid>.gz
실습 : 메모리 누수 탐지기 구현
여러분만의 간단한 메모리 누수 탐지기를 구현해보세요.
이 프로젝트를 통해 메모리 관리의 내부 동작을 깊이 이해할 수 있습니다.
- 사용자 정의 new와 delete 연산자 오버로딩
- 할당된 메모리 블록 추적
- 프로그램 종료 시 해제되지 않은 메모리 블록 보고
- 메모리 할당 위치(파일 이름, 라인 번호) 기록
- 스레드 안전성 고려
#include <iostream>
#include <map>
#include <cstdlib>
#include <new>
#include <mutex>
struct AllocationInfo {
size_t size;
const char* file;
int line;
};
std::map<void*, AllocationInfo> allocations;
std::mutex allocationMutex;
void* operator new(size_t size, const char* file, int line) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc();
std::lock_guard<std::mutex> lock(allocationMutex);
allocations[ptr] = {size, file, line};
return ptr;
}
void operator delete(void* ptr) noexcept {
if (!ptr) return;
{
std::lock_guard<std::mutex> lock(allocationMutex);
allocations.erase(ptr);
}
std::free(ptr);
}
#define new new(__FILE__, __LINE__)
void report_leaks() {
std::lock_guard<std::mutex> lock(allocationMutex);
if (allocations.empty()) {
std::cout << "No memory leaks detected.\n";
return;
}
std::cout << "Memory leaks detected:\n";
for (const auto& [ptr, info] : allocations) {
std::cout << "Leak: " << info.size << " bytes at " << ptr
<< " allocated in " << info.file << " line " << info.line << "\n";
}
}
int main() {
int* p1 = new int; // 이 할당은 추적됩니다
delete p1; // 정상적으로 해제됩니다
int* p2 = new int[10]; // 이 할당도 추적됩니다
// p2를 해제하지 않고 프로그램을 종료합니다
report_leaks();
return 0;
}
연습 문제
- 순환 참조로 인한 메모리 누수를 발생시키는 코드를 작성하고,
std::weak_ptr
를 사용하여 이를 해결해보세요. - 메모리 풀(memory pool)을 구현하여 작은 객체들의 빈번한 할당과 해제로 인한 성능 저하를 방지해보세요.
참고자료
- "Effective Modern C++" by Scott Meyers - 특히 스마트 포인터와 메모리 관리에 관한 항목들
- "C++ Concurrency in Action" by Anthony Williams - 멀티스레드 환경에서의 메모리 관리에 대해 다룸
- Valgrind 공식 문서
- AddressSanitizer 사용 가이드
- "The C++ Programming Language" by Bjarne Stroustrup - C++의 메모리 모델과 관리에 대한 깊이 있는 설명
- CppCon 발표 영상들 - YouTube에서 "CppCon memory management"로 검색하면 다양한 고급 주제의 발표를 찾을 수 있습니다.