리눅스 개발자를 위한 디버깅 기법 ⑤
메모리 관리 디버깅 기법
박재호 책임연구원 / 디비코
[ 입력 : 2006-07-04 오후 3:31:13 | 지면발행 : 2006년 7월호 96쪽]
출처 : http://www.embeddedworld.co.kr/article/view.asp?article_idx=7558
C#, 자바와 같은 고차원적인 프로그래밍 언어를 사용할 경우 메모리 관리와 가베지 컬렉션을 언어차원에서 제공하므로 프로그래머가 메모리에 대한 걱정을 전혀 할 이유가 없다. 하지만 C나 C++와 같은 프로그래밍 언어를 사용할 경우에는 효율을 높이기 위한 목적으로 메모리 관리를 직접 해야 하기 때문에 메모리에 대해 신경을 곤두세워야 한다. 비록 요즘 나오는 CPU는 대부분 가상 메모리 기능을 제공하므로 문제가 발생하더라도 해당 프로세스에 피해가 국한되긴 하지만, 전반적인 시스템 안정성을 저해하고 시스템 자원이 부족해지는 상황을 막기 위해서는 반드시 메모리 디버깅에 신경을 써야 한다. 물론 프로그래머가 튼튼한 실력을 바탕으로 철두철미하게 메모리 사용에 신경을 써서 프로그램을 작성하면 모든 문제를 예방할 수 있겠지만, 사람이 하는 일이므로 얼마든지 실수가 발생할 수 있기에 이를 감지해주는 방안이 필요하다. 이번 연재 기사에서는 동적 메모리 관리 디버깅을 도와주는 rmalloc을 살펴보기로 하자.
들어가는 말
메모리 관리 오류는 응용 프로그램과 시스템 동작에 예상하지 못한, 심지어는 치명적인 영향을 미칠 수 있다. 사용가능한 메모리가 감소함에 따라 프로세스와 시스템 전체가 정지하거나, 손상된 메모리로 인해 시스템이 알 수 없는 이유로 죽어 버린다. 시스템이 멈추지는 않더라도, 버퍼 넘침 현상으로 인해 시스템 보안이 취약해질 수도 있다. 설상가상으로 문제가 실제로 표출되기까지 며칠이 걸릴 수도 있다. 요즘 주 메모리는 GByte를 장착한 컴퓨터가 일반적이므로, 프로그램에서 메모리 누수 현상이 조금씩 생긴다면 응용 프로그램과 시스템에 증상이 나타나기까지 오랜 시간이 걸릴 것이다. 메모리 관리 오류는 꽤나 까다롭기 때문에 찾아내어 고치기가 매우 어려운 경우가 많다.
이런 문제점을 찾아내기 위해 등장한 메모리 관리 디버깅 도구는 IBM/Rational에서 만든 별도 재컴파일이 필요 없는 purify와 같은 상용 도구부터 시작한다. 또한 이번 연재에서 소개하는 프로그램에 손을 대어 동적 메모리 문제만 찾아내는 간단한 rmalloc과 같은 오픈 소스 도구에 이르기까지 기능과 성능이 천차만별이다.
강력한 메모리 디버깅 도구는 정적 배열에서 시작해 동적 메모리 할당에 이르기까지 거의 모든 버그를 실행 중에 바로 잡아낼 수 있다. 그렇기 때문에 메모리 할당과 해제를 수작업으로 수행해야 하는 C나 C++와 같은 프로그램 언어를 사용하는 개발자에게 많은 도움을 준다.
rmalloc은 오픈 소스 디버깅 도구 중에서도 크기도 작고 기능도 약한 축에 들어가지만 오히려 이런 특성으로 인해 임베디드 리눅스 환경으로 상대적으로 이전이 용이하기 때문에 선택했다. 물론 rmalloc을 사용해서 몇 만 라인을 넘어가는 대규모 프로그램을 효과적으로 디버깅하거나 타이밍이 중요한 실시간 응용 프로그램을 디버깅하기에는 문제가 있겠지만, 규모가 크지 않은 응용 프로그램을 빠르게 디버깅하거나 초보 개발자들이 메모리 문제를 미리 학습할 필요가 있을 경우라면 효과적으로 적용이 가능하다.
5회에서는 간략하게 C에서 사용하는 동적 메모리 함수를 살펴본 다음, 구체적인 rmalloc 사용법과 찾아낼 수 있는 버그 종류에 대한 테스트 예제를 살펴보기로 하자.
C에서 사용하는 동적 메모리 함수
리눅스 libc 라이브러리에서 제공하는 기본 메모리 관리 함수는 malloc(), calloc(), realloc(), free() 넷이다. 네 함수 모두 stdlib.h에서 함수를 선언한다.
·malloc()
malloc()은 초기화하지 않은 메모리 블록을 할당한다. 함수 선언은 다음과 같다.
void* malloc(size_t size)
·매개변수는 할당하려는 메모리 byte 수다. 할당에 성공하면, malloc()은 메모리 포인터를 반환한다. 어떤 이유에서든 메모리 할당에 실패하면(예를 들어, 시스템에 메모리가 바닥난 경우), malloc()은 NULL을 반환한다.
·calloc()
calloc()은 메모리에 배열을 할당하고 모든 메모리를 0으로 초기화한다. 반면 malloc()에서 할당하는 메모리는 초기화되어 있지 않다. calloc 함수 선언은 다음과 같다.
void* calloc(size_t nmemb, size_t size)
·첫 번째 매개변수는 배열 요소 개수이며, 두 번째 매개변수는 각 배열 요소 크기(byte)이다. malloc()과 마찬가지로, calloc()도 메모리 할당에 성공하면 포인터를 반환하고 실패하면 NULL을 반환한다.
·realloc()
realloc() 함수 선언은 다음과 같다.
void* realloc (void *ptr, size_t size)
·realloc()은 포인터가 가리키는 객체 크기를 두 번째 매개변수로 넘어온 새 크기로 변경한다. realloc()은 이동한 메모리 블록을 가리키는 포인터를 반환한다.
·free()
free()는 메모리 블록을 해제한다. 다음 함수 선언에서 보듯이, 포인터를 매개변수로 받아서 해당 메모리를 해제한다.
void free (void *ptr)
rmalloc 사용 방법과 동작 원리
다른 메모리 디버깅 라이브러리와 달리 rmalloc의 사용 방법은 정말로 단순하다. 디버깅을 원하는 소스 코드에 #define MALLOC_DEBUG 한 줄을 추가한 다음에 rmalloc 패키지에 들어있는 rmalloc.h를 인클루드하고, 링크 시점에서 rmalloc.c를 컴파일한 rmalloc.o를 링크시키기만 하면 된다. 일반 프로그램을 실행하듯이 rmalloc으로 디버깅할 프로그램을 동일하게 실행하면 된다. 물론 gdb와 같은 대화식 디버거도 똑같이 쓸 수 있다.
동작 원리 역시 사용 방법만큼이나 단순하다. rmalloc.h에는 앞서 설명한 일반적인 동적 메모리 관리 함수인 malloc, calloc, realloc, free, strdup 함수를 감싸고 있는 매크로가 정의되어 있으므로, 이 헤더 파일을 인클루드 시킨 소스 코드 내부에서 호출하는 모든 동적 메모리 관리 함수는 원래 libc 함수 대신 디버깅 함수를 호출하게 된다. 디버깅 버전 함수는 할당한 버퍼 직전과 직후에 특별한 표시를 해서 돌려주며, 나중에 이 부분이 손상당했을 경우에 문제가 생긴 파일 이름과 행 정보를 추가해서 출력하면서 강제로 종료한다. rmalloc이 강제로 중단시킨 프로그램은 gdb와 같은 대화식 디버그를 사용해서 좀 더 정밀하게 검사할 수 있다.
노파심에서 말하지만 디버깅이 끝나면 #define MALLOC_ DEBUG를 제거하고 다시 컴파일해서, 디버깅 함수가 아닌 원래 libc 함수를 호출하도록 만들어서 다시한번 테스트를 수행하기 바란다. 향후 디버깅이 계속 필요할 경우에는 MALLOC_DEBUG만 제거하고 그렇지 않다면 해당 소스 코드에서 rmalloc.h 헤더 파일 인클루드를 빼고 Makefile에서 rmalloc.o를 제외시키면 된다.
rmalloc 테스트 예제 소개
그러면 rmalloc에서 찾아낼 수 있는 다섯 가지 전형적인 예를 소개하고자 한다. 각 예는 rmalloc 패키지에 들어있는 rtest.c에서 발췌한 내용이다.
사례 1: 동적 메모리 1byte 오버플로우(off-by-one) 오류
주로 동적 메모리를 할당한 다음에 문자열 크기 계산 과정에서 실수할 때 나타나는 오류이다. 흔히 ‘\0’로 끝난다는 사실을 망각하고 문자열을 사용할 경우에 많이 나타난다. Test1에서는 3byte짜리(‘0’, ‘1’, ‘\0’) str에 4byte(‘0’, ‘1’, ‘2’, ‘\0’)짜리 문자열을 복사하는 실수를 저질렀다.
static void Test1(void)
{
char *str = strdup("01");
strcpy(str, "012"); /* wrong! */
free(str);
}
사례 2: 동적 메모리 오버플로우 오류
할당한 메모리를 넘어서서 문자열을 복사할 경우에 나타나는 오류이다. Test1에서는 2byte짜리 str에 8byte짜리 문자열을 복사하는 실수를 저질렀다.
static void Test2(void)
{
char *str = strdup("0");
strcpy(str, "012long"); /* wrong! */
free(str);
}
사례 3: 동적 메모리 포인터 색인 오버플로우(1byte짜리 영역을 할당해놓고 범위를 넘어섬)
C에서는 포인터와 배열을 섞어서 사용할 수 있으며, 배열 내 색인을 잘못 지정할 경우 범위를 넘어설 수 있다. Test3에서는 char 포인터를 가리키는 배열을 1개만 잡은 다음에 색인을 1(두 번째 원)로 넘기는 실수를 저질렀다.
static void Test3(void)
{
char **arr = malloc(1*sizeof(char *));
char *bla = strdup("Kaputt!");
arr[1] = bla; /* wrong! */
free(arr); /* last chance to find */
free(bla);
}
사례 4: 같은 메모리 해제 두 번 하기
동일한 메모리를 두 번 해제할 경우에 문제가 발생한다. 특히 해커들이 이런 취약점을 노려서 악의적인 코드를 삽입하기도 하므로 각별히 조심하기 바란다. Test4에서는 bla[0]을 두 번 해제하는 문제점을 내포하고 있다. 여기서 NULL을 해제할 경우가 문제가 되는지 그렇지 않은지는 나중에 설명할 ALLOW_FREE_NULL 매크로로 제어할 수 있다.
static void Test4(void)
{
unsigned int u;
int i;
char *bla[TEST_SIZE];
unsigned int count = TEST_SIZE;
char *foo[20];
void *pending;
memset(bla, 0, sizeof(bla));
srand(time(NULL));
while (count > 0) {
u = (u + rand()) % TEST_SIZE;
if (bla[u] == NULL) {
bla[u] = malloc(u+1);
assert(bla[u] != NULL);
count--;
}
}
for (i = TEST_SIZE-1; i >= 0; i--) {
bla[i] = realloc(bla[i], 2*i+2);
}
pending = bla[0];
count = TEST_SIZE;
while (count > 0) {
u = (u + rand()) % TEST_SIZE;
if (bla[u] != NULL) {
free(bla[u]);
bla[u] = NULL;
count--;
}
}
free(pending); /* wrong! */
}
사례 5: 잘못된 메모리 번지 해제
터무니없는 포인터가 가리키는 번지를 해제할 경우 시스템 동적 메모리 관리를 엉망으로 만들 수 있다. 역시 보안 문제를 초래할지도 모르므로 조심해야 한다. Test5에서는 0x12345678이라는 터무니없는 값을 넘기는 실수를 저지르고 있다.
static void Test5(void)
{
free((void *)0x12345678); /* wrong! */
}
rmalloc 테스트 결과
앞서 소개한 다섯 가지 예를 rmalloc으로 점검할 경우에 어떤 결과가 나타나는지 살펴보기로 하자.
사례 1: 동적 메모리 1byte 오버플로우(off-by-one) 오류
앞쪽 a5a5a5a5 표식이 00a5a5a5a로 변한 사실을 토대로 missing null byte라고 문제점을 정확하게 알려주고 있다.
should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
is: 00a5a5a5 5b5b5b5b abababab aa55aa55
block was allocated in rtest.c:142 [3 Bytes, generation 1]
error was detected in rtest.c:144
Looks like string allocated one byte too short
(missing the null byte)
사례 2: 동적 메모리 오버플로우 오류
사례 1과 유사한 점검 방법으로 rmalloc은 ending with “2long”이라고 너무 긴 문자열이 들어왔음을 정확하게 알려준다.
should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
is: 326c6f6e 67005b5b abababab aa55aa55
block was allocated in rtest.c:161 [2 Bytes, generation 1]
error was detected in rtest.c:163
Looks somewhat like a too long string,
ending with "2long"
사례 3: 동적 메모리 포인터 색인 오버플로우(1byte짜리 영역을 할당해놓고 범위를 넘어섬)
앞서 사례 1, 2처럼 이유를 명쾌하게 밝히지는 않지만 rmalloc은 어디서 어떤 문제가 발생했는지 확실하게 알려주고 있다.
should be: a5a5a5a5 5b5b5b5b abababab aa55aa55
is: e02f0608 5b5b5b5b abababab aa55aa55
block was allocated in rtest.c:181 [4 Bytes, generation 1]
error was detected in rtest.c:185
First 4 bytes of overwritten memory can be interpreted
as a pointer to a block allocated in:
rtest.c:182 [8 Bytes, generation 2]
사례 4: 같은 메모리 해제 두 번 하기
rmalloc은 double or false delete라는 메시지로 같은 메모리를 두 번 해제했음을 명시적으로 알려준다
Heap adress of block: 0x805f270
Detected in rtest.c:240
Trying identification (may be incorrect!):
Allocated in rtest.c:225 [2 Bytes]
사례 5: 잘못된 번지 해제
rmalloc은 double or false delete라는 메시지로 잘못된 메모리 번지를 해제했음을 명시적으로 알려준다.
Heap adress of block: 0x12345678
Detected in rtest.c:258
메시지만으로는 확인이 어려운 경우도 있으므로 반드시 해당 소스 코드를 살펴서 문제의 근본적인 원인을 찾아서 공격해 들어가는 습관을 길러야 한다. 동적 메모리와 관련해서 정말 내가 무엇을 원하고 무엇을 하고 있는지 확신을 품고 프로그램과 디버깅에 나서지 않으면 여러 가지 복잡한 문제가 벌어지기 쉽다.
rmalloc에서 제공하는 스위치
rmalloc은 행동 양식을 바꾸기 위한 다양한 매크로를 제공한다. 다음에 소개하는 매크로 설정은 rmalloc.c에서 변경한 다음에 rmallo.o를 빌드해서 디버깅 대상 응용 프로그램과 링크만 다시 하면 된다. 헤더 파일인 rmalloc.h에 넣지 않은 이유는 각 소스 코드마다 rmalloc 행동 양식이 달라질 경우에 혼동을 일으킬 수 있기 때문이다.
rmalloc에서 제공하는 추가 매크로
rmalloc 패키지는 malloc 계열 함수 이외에도 내부적으로 제공하는 추가적인 매크로를 제공한다. 기본적인 malloc 계열 함수 사용법과는 달리 이 매크로를 적용하기 위해서는 원본 소스 코드를 뜯어고쳐야 하는 불편함이 있다.
RM_TEST
모든 할당 받은 블록에 대해 새로 점검을 수행한다. 이 매크로를 사용하기 위해서는 RM_TEST_DEPTH를 1이상으로 설정해야 한다. 사용법은 다음과 같다.
RM_TEST; /* tests ALL memory chunks on heap */
RM_STAT
모든 할당 받은 블록에 대해 새로 점검을 수행한 다음에 표준 오류로 할당 받은 블록 통계를 출력한다. 이 매크로를 사용하기 위해서는 RM_TEST_DEPTH를 1이상으로 설정해야 한다. 사용법은 다음과 같다.
RM_STAT; /* shows allocated memory */
스위치 이름 설정값 설명
RM_TEST_DEPTH 0, 1, 2 0: 최소, 1: 통계 정보 제공/메모리 해제 시점에 검사, 2: malloc 계열 함수 호출 시점에 검사
GENERATIONS ON/OFF 디버거에서 rmalloc_generation() 중단점을 걸어 어떤 함수 스택이 메모리 할당을 하는지 추적하는 플래그
ELOQUENT ON/OFF 확장 메모리 할당 정보 출력 유무
WITH_FLAGS ON/OFF 다음에 설명할 RM_SET 활성 유무
ALLOW_REALLOC_NULL ON/OFF realloc(NULL) 허용 유무
ALLOW_FREE_NULL ON/OFF free(NULL) 허용 유무
BREAK_GENERATION_COND 외부 환경 변수 참조 GENRATIONS를 활성화 했을 경우 rmalloc_generation() 호출을 수행하기 위한 조건 정의
MAX_STAT_GENERATIONS 숫자 GENRATIONS를 활성화 했을 경우 통계에서 출력할 최대 generation 횟수
RM_RETAG
종종 메모리 영역을 받아 몇몇 기본값을 설정한 다음에 사용자에게 돌려주는 함수를 사용하는 경우가 있다. 이런 특별한 함수를 사용할 경우 rmalloc이 메모리 추적에 어려움을 겪을 가능성이 있으므로, 매크로를 수행한 시점으로 위치를 초기화하는 RM_RETAG 함수를 제공한다. 사용법은 다음과 같다.
struct complicated *cpointer = get_new_complicated _struct(any_arg);
RM_RETAG(cpointer); /* file pos is now set to here */
또는
struct complicated *cpointer = RM_RETAG(get_new_ complicated_struct(any_arg));
RM_SET
할당 받은 메모리 영역에 특별한 플래그를 붙인다. 현재 RM_STATIC과 RM_STRING만 사용할 수 있다.
- RM_STATIC: 해제하지 않을 정적으로 고정된 메모리를 의미한다. 사용법은 다음과 같다.
static char *buffer = NULL;
static int length = 0;
[...]
if (newlength > length) {
if (buffer == NULL) {
buffer = malloc(length = newlength);
RM_SET(buffer, RM_STATIC); /* this is never freed */
} else {
buffer = realloc(buffer, length = newlength);
/* RM_SET(buffer, RM_STATIC); */
}
if (buffer == NULL) {
error(NOMEM);
}
}
- RM_STRING: 문자열을 담는 메모리를 의미한다. ELOQUENT 모드에서만 통계에 잡히며, strdup는 자동으로 이 플래그를 설정한다. 사용법은 다음과 같다.
struct numstr {
int number;
char *name;
};
[...]
struct numstr *foo = malloc(sizeof(struct numstr));
foo->name = calloc(1, 2);
foo->name[0] = 'X'; /* sheer nonsense */
RM_SET(foo->name, RM_STRING);
결론
지면 관계상 동적 메모리 디버깅을 다루는 다양한 오픈 소스 소프트웨어 중에서 rmalloc만 다루었다. 기본적인 문제점을 분석하는 과정에서 rmalloc으로도 충분하지만, 응용 프로그램이 커지고 복잡한 상황에 직면할 경우에는 추가적으로 다른 동적 메모리 디버깅 라이브러리를 도입할 필요도 있다. 참고 문헌에 ‘추가적인 메모리 할당 디버깅 라이브러리 정보’관련 URL을 정리해 놓았으므로, 한번쯤 각 라이브러리 특징을 파악하면 나중에 도움이 될 것이다. 여기서 각 라이브러리마다 사용법은 대동소이하지만, 혹시 실제 적용하는 과정에서 추가적인 한글 정보가 필요하면 ‘리눅스 디버깅과 성능 튜닝’ 4장을 통해 MEMWATCH, YAMD, Electric Fence, Valgrind에 대한 세부적인 정보를 얻기 바란다.
참고문헌
* “리눅스 디버깅과 성능 튜닝”, 박재호, 이해영 역, 에이콘 출판사 2006년: 4장에서 다양한 오픈 소스 메모리 관리 라이브러리를 다룬다.
* http://www.hexco.de/rmdebug/ rmalloc 디버거 홈페이지이다.
* 추가적인 메모리 할당 디버깅 라이브러리 정보
http://www.linkdata.se/sourcecode.html MEMWATCH 웹 페이지
http://www.cs.hmc.edu/~nate/yamd/ YAMD 웹 페이지
http://perens.com/FreeSoftware/ Electric Fence 웹 페이지
http://valgrind.org/ Valgrind 웹 페이지
http://dmalloc.com/ Dmalloc 웹 페이지