제공 :
한빛 네트워크
저자 : Richard Reese
역자 : 정지용
원문 :
Memory Deallocation Issues in C
동적 메모리가 해제되면, 그걸로 끝인 것 같아 보입니다. 메모리가 새는 것을 막았고, 자신감 있게 다음 문제로 넘어가면 될 것 같습니다. 어떤 경우에는 그럴 수도 있겠지요. 하지만 더 생각해봐야할 문제들이 있습니다. 민감한 데이터를 다루고 있을 수도 있고, 프로그램이 종료되기 직전에 메모리를 해제할 지 고민해 봐야할 수도 있습니다. 이 글에서 이런 문제를 비롯한 여러 문제들에 대해서 살펴봅시다.
힙과 시스템 메모리
힙(heap)은 일반적으로 메모리 관리에 운영체제 함수를 사용합니다. 힙의 크기는 프로그램이 시작될 때 정해질 수도 있고, 프로그램이 실행됨에 따라 커질 수도 있습니다. 하지만 free 함수가 호출될 때 힙 관리자가 꼭 메모리를 운영체제에 반환할 필요는 없습니다. 간단하게 프로그램이 다음에 쓸 수 있도록 해놓기만 하면 됩니다. 따라서 프로그램이 메모리 할당과 해제를 했을 때, 일반적으로 운영체제 관점에서는 해제된 메모리만큼 프로그램의 메모리 사용량이 줄어들지 않습니다.
중복 해제
메모리는 보통 free 함수로 해제합니다. 동일한 메모리를 두 번 해제하려고 하면 문제가 발생하겠죠. 한 메모리 블록을 두 번 해제하는 것을 중복 해제(double free)라고 합니다. 아래 코드에서 이 문제를 볼 수 있습니다:
char *name = (char*)malloc(...);
...
free(name); // 첫 번째 해제
...
free(name); // 중복 해제
포인터 값이 다른 변수에 들어가면 중복 해제를 더 알아채기 힘들어집니다:
char *name = (char*)malloc(...);
char *tmp = name;
...
free(name); // 첫 번째 해제
...
free(tmp); // 중복 해제
지립(zlib) 압축 라이브러리의 초기 버전에서는 중복 해제 문제로 인해 서비스 거부 공격이나 실행 코드 삽입이 가능했었습니다. 하지만 이런 일은 매우 드문 것이고, 이 취약점은 이후 릴리즈에서 이미 수정되었습니다. 취약점에 대해 더 궁금한 점은
cert.org을 참고하십시오.
중복 해제 문제를 해결하는 가장 간단한 방법은 메모리를 해제한 후 항상 포인터에 널(NULL)을 할당하는 것입니다. 널 포인터를 해제하려는 이후의 시도는 대부분의 힙 관리자들이 무시하니까요.
char *name = (char*)malloc(...);
...
free(name);
name = NULL;
제 책 C 포인터 이해와 사용(
Understanding and Using C Pointers)의 3장에서, 해제된 포인터에 자동으로 NULL을 할당하는 자신만의 free 함수를 만드는 방법을 설명하고 있습니다. 포인터의 포인터에 대한 좋은 예제이기도 하지요.
민감한 데이터 지우기
메모리의 민감한 데이터는 더 이상 필요가 없어졌을 때 덮어쓰는 편이 좋습니다. 프로그램이 종료되면, 대부분의 운영체제는 프로그램이 사용했던 메모리를 0으로 덮어쓰거나 하는 식의 처리를 하지 않습니다. 내가 사용했던 메모리 공간이 다른 프로그램에 할당될 수 있고, 당연히 그 안의 내용도 접근 가능해집니다. 민감한 데이터를 덮어씀으로써, 민감한 데이터를 저장하기 위해 사용되었던 주소 공간에서 다른 프로그램이 의미 있는 정보를 빼가는 것을 어렵게 만들 수 있습니다. 아래 코드는 프로그램에서 민감한 데이터를 0으로 덮어쓰는 모습을 보여줍니다:
char name[32];
int userID;
char *securityQuestion;
// 값 할당
...
// 민감한 정보 삭제
memset(name,0,strlen(name));
userID = 0;
memset(securityQuestion,0,strlen(securityQuestion));
만약 name 변수가 포인터로 선언되었다면, 아래와 같이 해제하기 전에 메모리 내용을 지워야합니다.
char *name = (char*)malloc(...);
...
memset(name,0,strlen(name));
free(name);
기밀 정보를 다루도록 인증 받은 많은 운영체제들에서는 프로그램이 더 이상 사용하지 않을 때 자동으로 메모리를 지운다는 점에 주목할 필요가 있습니다. 이런 데이터 자동 삭제에는 운영체제의 추가 부담이 있습니다.
프로그램 종료 시 메모리 해제
운영체제는 프로그램의 자원을 관리할 책임이 있고, 이는 메모리도 마찬가지입니다. 프로그램이 종료될 때, 운영체제는 이 프로그램의 메모리를 다른 프로그램에 재할당해야 합니다. 종료된 프로그램 메모리에 오류가 있는지 없는지와 같은 상태는 고려 대상이 아닙니다. 사실 프로그램이 종료되는 이유 중 하나가 메모리 오류입니다. 프로그램이 비정상적으로 끝나면, 해제가 불가능할 수도 있습니다.
이러한 점 외에도, 프로그램이 정상적으로 끝날 때 메모리를 해제하는 것이 좋은 이유들이 있습니다:
- 성실한 프로그래머라면 품질을 위해 메모리를 해제하고 싶어 할 수도 있습니다. 더 이상 사용하지 않는 메모리를 해제하는 것은 좋은 습관이고, 이는 프로그램이 종료될 때도 마찬가지입니다.
- 메모리가 새는 문제나 비슷한 문제들을 찾는 도구를 사용한다면, 메모리를 해제하는 편이 결과물을 깔끔하게 만듭니다.
- 비교적 단순한 몇몇 운영체제에서는, 운영체제가 자동으로 메모리를 재사용하지 않습니다. 이런 경우 종료 전 메모리를 재사용할 수 있도록 만드는 것이 프로그램의 책임일 수 있습니다.
- 마지막으로, 프로그램의 다음 버전에서는 프로그램 맨 마지막에 코드를 추가할 수도 있습니다. 앞에서 메모리를 해제하지 않았다면, 문제가 생길 수 있습니다.
동전의 반대쪽 면도 살펴봅시다. 프로그램 종료 전 모든 메모리를 꼭 해제하는 것은……:
- 가치에 비해 너무 힘들 수 있습니다.
- 복잡한 구조의 메모리를 해제하는 것은 시간도 많이 들고 복잡할 수 있습니다.
- 프로그램의 크기를 키울 수 있습니다.
- 따라서 동작시간도 늘어날 수 있죠.
- 버그를 만들어 낼 기회도 늘어납니다.
프로그램 종료 전 메모리를 꼭 해제해야하는지는 프로그램에 따라 다르다고 하겠습니다.
요약
보신 바와 같이 메모리를 해제할 때는 생각할 문제들이 많이 있습니다. 단순히 free 함수만을 사용하는 것만으로는 충분치 않습니다. 이 문제들 중 많은 수가 realloc 함수 같은 다른 메모리 함수들에도 있습니다. 전체 맥락에서 메모리가 어떻게 사용되는지를 이해함으로써 프로그램에서 메모리를 어떻게 사용해야 할지에 대한 깨달음을 얻을 수 있을 것입니다.