Address Sanitizer 를 이용하여 힙 메모리 오류 디버깅하기
C/C++ 언어로 작성된 프로그램은 다양한 유형의 오류가 발생할 수 있습니다. 대부분의 경우 디버거의 브레이크 포인트, Memory Watch Point 등의 기능으로 쉽게 디버깅할 수 있지만, 스레드간 경합에 의한 오류, 힙 메모리 오염 등은 디버깅하기가 쉽지 않습니다. 특히 동적 메모리 할당 시 사용되는 힙 메모리가 오염된다면 프로그램은 전혀 상관 없는 위치에서 크래시할 수 있기 때문에 원인을 찾아 수정하기가 매우 어렵습니다.
힙 메모리가 망가진 경우 프로그래머는 대개 힙 메모리를 망가트린 곳이 아니라 망가진 힙 메모리를 접근하여 크래시하는 위치에서 디버깅을 시작합니다. 프로그래머는 엉뚱한 코드를 분석하며 긴 시간을 허비하기 십상입니다. 이 글에서는 힙 메모리 오염을 만드는 위치를 찾아주는 Address Sanitizer 라는 메모리 오류 탐지기를 이용하여 힙을 망가트리는 대표적인 원인 중 하나인 Double-Free 문제를 빠르게 디버깅하는 방법을 소개합니다.
Address Sanitizer 는 Ubuntu 와 Centos 에서 패키지로 제공되므로 패키지 매니저를 통해서 설치하실 수 있습니다.
Ubuntu 사용시
$ sudo apt-get install libasan2
Centos 사용시
$ sudo yum install libasan
Address Sanitizer 는 다른 메모리 할당자 라이브러리와 다르게 라이브러리 링크만으로 사용할 수 없습니다. 링크 단계 뿐만 아니라 컴파일 단계에서도 컴파일러에게 기능을 켜도록 알려주기 위해서 GCC 플래그 “-fsanitize=address -lasan” 를 추가하셔야 됩니다. Address Sanitizer 가 단순한 라이브러리가 아니라 컴파일러의 도움을 받기 때문입니다.
$ gcc -c my_source.cc -fsanitize=address $ gcc -o my_executable my_source.o -fsanitize=address -lasan
사용 예시: Double-Free 오류 찾기
아래 코드는 “new []” 로 할당한 메모리를 “delete []” 로 두 번 해제하는 오류를 가지고 있으며 힙 메모리를 망가트립니다.
<코드 1>
이 프로그램을 실행하면 힙 메모리가 망가지며, 메모리 할당자에 따라 다른 결과를 만들어냅니다. Address Sanitizer 를 사용했다면 프로그램은 실행 후 즉시 크래시하고 아래 내용이 콘솔에 출력됩니다.
<그림 1>
출력된 내용은 이 코드를 디버깅하기 위한 3 가지 정보를 제공합니다.
1. 메모리 할당 위치
<그림 2>
22~27 번째 줄은 메모리가 할당된 위치와 스레드를 보여줍니다. <코드 1> 의 8 번째 줄에 해당하는 호출 스택을 볼 수 있습니다. (출력된 스레드 이름은 “pthread_setname_np()” 등을 함수로 붙여지며, 이 예제는 iFunEngine 으로 작성되었고 “event” 라는 이름을 갖는 스레드에서 실행됐습니다.)
2. 메모리 해제 위치
<그림 3>
12~19 번째 줄은 포인터 p 를 해제한 위치와 스레드를 출력합니다. <코드 1> 의 4 번째 줄에 해당하는 호출 스택을 볼 수 있습니다.
3. 두 번째 메모리 해제 위치(Double-Free)
<그림 4>
1~8 번째 줄은 프로그램이 <코드 1> 의 8 번째 줄에서 할당한 포인터 p 를 두 번째 해제를 시도한 위치와 스레드가 출력됩니다. <코드 1> 의 10 번째 줄에 해당하는 호출 스택을 볼 수 있습니다.
Address Sanitizer 가 출력한 정보를 토대로 프로그래머는 메모리 할당 위치, 해제 위치, 그리고 문제의 Double-Free 가 발생하는 두 번째 해제 위치를 찾을 수 있습니다. 이는 프로그램을 어떻게 수정해야할지 결정하는데 충분합니다.
기본 할당자나 Tcmalloc, Jemalloc 과 같이 성능에 초점을 맞춘 할당자의 경우 아무런 정보도 주지 않거나, 두 번째 해제 위치 정도만 알 수 있을 것입니다. (단, 디버그 기능을 활성화 한 경우 조금 더 많은 정보를 얻을 수 있습니다.) VisualStudio, GDB 와 같은 인터랙티브 디버거는 이러한 문제를 해결할 직접적인 기능을 제공하지 않으며 Valgrind memcheck 와 같은 전문적인 도구는 성능 저하가 심하다는 단점이 있습니다. 이는 컴파일 타임에 정보 수집을 위한 코드를 삽입하여 성능 저하가 적은 Address Sanitizer 의 큰 장점 중 하나 입니다.
Address Sanitizer 는 위에서 설명한 Double-Free 외에도 메모리 릭, 해제된 메모리 접근, 배열 인덱스 오류 등 거의 모든 메모리 오류를 찾을 수 있는 기능을 제공합니다. 최신 컴파일러에서 기본으로 지원하고 있어서 쉽게 사용할 수 있습니다. 더 자세한 설명은 https://github.com/google/sanitizers/wiki/AddressSanitizer 를 참고하시기 바랍니다.