icon안동민 개발노트

메모리 누수 탐지


메모리 누수의 이해

 메모리 누수는 프로그램의 성능을 저하시키고 잠재적으로 시스템 불안정을 야기할 수 있는 심각한 문제입니다. 이 장에서는 C++ 프로그램에서 메모리 누수를 탐지하고 방지하는 다양한 기법과 도구를 심도 있게 살펴보겠습니다.

 메모리 누수는 프로그램이 더 이상 사용하지 않는 메모리를 해제하지 않을 때 발생합니다. 이로 인해 프로그램이 점점 더 많은 메모리를 소비하게 되어 결국 시스템 자원을 고갈시킬 수 있습니다.

 메모리 누수의 유형

  1. 단순 누수 : 할당된 메모리를 해제하지 않는 경우
  2. 순환 참조 누수 : 객체들이 서로를 참조하여 해제되지 않는 경우
  3. 캐시 누수 : 캐시된 객체를 적절히 관리하지 않는 경우
  4. 콜백 누수 : 등록된 콜백을 제거하지 않는 경우

 메모리 누수 예제

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

실습 : 메모리 누수 탐지기 구현

 여러분만의 간단한 메모리 누수 탐지기를 구현해보세요. 이 프로젝트를 통해 메모리 관리의 내부 동작을 깊이 이해할 수 있습니다.

  1. 사용자 정의 new와 delete 연산자 오버로딩
  2. 할당된 메모리 블록 추적
  3. 프로그램 종료 시 해제되지 않은 메모리 블록 보고
  4. 메모리 할당 위치(파일 이름, 라인 번호) 기록
  5. 스레드 안전성 고려
예시 코드
#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;
}

연습 문제

  1. 순환 참조로 인한 메모리 누수를 발생시키는 코드를 작성하고, std::weak_ptr를 사용하여 이를 해결해보세요.
  2. 메모리 풀(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"로 검색하면 다양한 고급 주제의 발표를 찾을 수 있습니다.