정적코드분석/코딩표준

메모리 누수 (Memory Leak)란?

이번에 소개할 소프트웨어 오류 유형은 바로 메모리 누수(Memory Leak)입니다.

‘메모리 누수’ 란?

메모리 누수는 프로그램이 메모리를 할당 후, 해제하지 않아서 시스템의 메모리를 고갈시키는 소프트웨어 오류입니다. 당장 프로그램이 비정상적으로 종료되지 않기 때문에 미처 메모리 누수가 있다는 사실을 알아차리지 못하거나, 자주 종료와 부팅을 반복하는 PC용 어플리케이션에서는 문제를 찾아도 문제 해결 순위에서 후 순위로 밀리곤 합니다. 하지만 메모리 누수 역시 소프트웨어에 있어서는 독과 같은 존재입니다. 다만 버퍼 오버플로우 같은 소프트웨어 오류는 발생 즉시 바로 프로그램이 죽는 맹독이라 한다면, 메모리 누수는 오랜 기간 동안 서서히 프로그램뿐만 아니라 해당 프로그램이 구동되는 시스템까지 죽이는 수은이라고 할 수 있겠죠. (여담으로, 사분오열되어 있는 중국을 통일한 진시황 역시 장기간에 걸친 수은 복용으로 사망했다는 이야기도 있습니다.)

출처 : http://www.lowflo.ie/
출처 : http://www.lowflo.ie/

다음 시스템에 위험합니다

이렇게 서서히 시스템을 말려 죽이는 소프트웨어 오류가 바로 메모리 누수지만, 컴퓨터는 사람이 아니기에 단순히 시스템을 다시 시작하는 것으로 간단하게 문제를 막을 수 있습니다. 하지만 한 번 시스템이 가동되면 계속 가동시켜야 하는 시스템에서는 매우 치명적인 문제를 일으킵니다. 예를 들어 1년에 한 번 끌까 말까 하는 서버가 되겠죠. 또한 시스템 메모리가 매우 제한적인 임베디드 장비에도 큰 문제가 됩니다. 실제로 모 회사 서버는 메모리 누수 문제를 해결하지 못해서 지속적으로 재부팅을 하고 있던 경우도 있었습니다.

Tomcat memory usage, imaged by http://www.flickr.com/photos/plutor/3248419938/ under CCL BY 2.0
Tomcat memory usage, imaged by http://www.flickr.com/photos/plutor/3248419938/ under CCL BY 2.0

어떻게 죽는가? 분석

C 소스 코드를 보면서 얘기하겠습니다.

char* read_buffer(FILE *file, const int size)
{
  int foo = 0;
  char* buf = (char*) malloc(size);

    if (buf == NULL)
    {
      return NULL;
    }
    if (fread(buf, 1u, size, file) != size)
    {
      return NULL;
    }
    return buf;
}

함수 내에서 선언해서 사용하는 변수는 지역변수로 컴퓨터 구조의 스택이라는 자료구조 내에 보관됩니다. 위 코드 내의 정수형 foo가 이 경우에 해당되는데 이 변수를 지역변수라고 합니다. 지역 변수는 함수가 종료될 때, 자동으로 사라집니다. 문제가 되는 것은 buf 변수입니다. 이 변수는 malloc 함수를 통해서 힙(Heap) 영역의 메모리를 할당받아서 사용하는데, 이 힙 영역의 메모리를 할당 받으면 명시적으로 메모리 해제를 해줘야 합니다. 하지만 fread 함수를 통해서 파일 읽기가 실패하면 11번 째 줄에서 NULL을 리턴하고 함수가 종료되는데요, 바로 여기서 메모리 누수가 발생합니다. 힙 영역의 메모리를 할당 받은 buf 변수가 free를 통해서 해제되지 않았기 때문입니다. 이 경우 프로그램이 종료되어도 해당 메모리 공간을 다시 사용할 수가 없으며, 이런 메모리 누수가 누적되면 결국 시스템 전체의 메모리 부족 현상이 발생할 수 있습니다.

아래와 같은 경우도 생깁니다.

char *foo = (char *) malloc(100);
char *bar = (char *) malloc(200);
bar = foo;

이 경우는 bar 포인터 변수가 힙 영역에 할당된 메모리 200바이트를 가리키고 있는데, bar의 주소 값에 foo의 주소 값을 대입하게 되면 이전에 bar 변수를 통해 할당한 힙 영역의 메모리 200바이트에 접근할 방법이 사라집니다. 이 경우에도 메모리 누수가 생깁니다.

typedef struct _my_struct {
char *foo;
int bar;
} my_struct;

my_struct *entity = (my_struct *)malloc(sizeof(my_struct));
entity->foo = (char *)malloc(100);

free(entity);

이 경우에는 구조체 my_struct의 필드인 foo가 힙 메모리 영역을 100바이트 할당 받았으나, 그 부모인 entity 가 먼저 해제되어서 foo에 접근해서 해제할 방법이 없어진 경우입니다. 이 코드를 실행하면 100바이트의 메모리 누수가 발생합니다.

char* get_buffer(void)
{
  return (char *)malloc(100);
}

void test_func()
{
  get_buffer();
}

위 코드의 경우 get_buffer() 함수가 힙 메모리에서 100바이트를 할당해서 그 포인터를 반환하지만 test_func() 함수에서는 이를 받는 변수가 없습니다. 이 경우에도 힙 메모리에 할당된 100바이트에 접근할 방법이 사라지기 때문에 메모리 누수가 발생합니다.

찾기 힘들다

위의 코드는 메모리 누수가 어떻게 일어나는지를 한 눈에 볼 수 있도록 단순화 시켜서 메모리 누수 원인을 빠른 시간 안에 정확하게 찾을 수 있습니다. 하지만 일반적으로 방대한 소스코드로 개발된 복잡한 프로그램에서 메모리 누수를 찾기란 매우 어렵습니다. 시스템 자원을 감시하거나 메모리 할당 실패 시 별도의 로그를 기록하지 않으면 특히 더 찾기 어렵습니다. 오류를 재현하기도 힘들 뿐더러 불규칙적인 시간과 증상을 보이면서 시스템이 장애를 일으키기 때문에 원인이 정확하게 무엇인지 추정할 수 없게 만듭니다.?메모리 누수는 프로그램뿐만 아니라 전체 시스템에 영향을 주기 때문에 해당 컴퓨터에서 작동 중인 모든 소프트웨어를 다 검사해야 하기도 합니다.

자바, 안드로이드, Objective-C 에도 존재

예제로 C 언어로 된 코드를 사용했지만 메모리 누수는 비단 C와 C++에만 존재하는 것이 아닙니다. 동적으로 할당한 메모리 중 필요 없게 된 영역을 해제하는 가비지 콜렉터(Garbage Collector) 기법을 이용하는 자바와 안드로이드에도 엄연히 메모리 누수가 존재합니다. 다만 앞서 소개한 C/C++ 코드에서 같이 할당한 힙 영역을 접근할 수 있는 포인터가 사라지는 경우에는 자바의 가비지 콜렉터가 동작해서 할당한 힙 영역을 다시 사용할 수 있게 해줍니다. 자바의 메모리 누수가 문제가 되는 경우는 Loitering Object 입니다. Loitering 의 뜻은??’어슬렁거리다, 서성이다’ 라는 뜻인데요, 아래의 코드에서 나타날 수 있습니다.

ArrayList array = new ArrayList();
SomeObject obj = new SomeObject();
array.add(obj);
.... // something to work

이 코드는 ArrayList형 변수 array에 SomeObject라는 객체를 추가했으나, 작업 후 array 내부의 객체를 제거하지 않음으로서 메모리 누수가 발생하는 코드입니다. ?이 경우 obj는?Loitering Object가 되서 obj가 할당한 메모리가 시스템으로 반환되지 않고 계속 메모리를 점유하게 됩니다.

보안도 위협

이 메모리 누수는 대부분의 경우 시스템을 전반적으로 불안정하게 만들지만, 때에 따라서는 보안에도 큰 위협이 될 수 있습니다. 메모리 누수가 있는 프로그램을 공격하여 시스템을 중단시키거나, 임의의 코드를 실행할 수 있으며, 메모리 부족 조건 하에서 발생하는 예기치 못한 동작을 이용해서 시스템을 공격할 수도 있습니다.

메모리 누수 탐지 방법

Java와 같은 언어에서는 메모리 누수를 탐지하는 도구가 있고 C/C++의 경우 프레임워크에서 지원하는 라이브러리가 있습니다. 윈도우즈의 CRT 라이브러리가 그 예가 되겠죠. 유명한 정적 코드 분석도구에서도 메모리 누수를 탐지하는 기능을 제공합니다.

2 Comments

  1. 힙영역에 할당한 메모리는 프로그램이 종료되는 경우 보통 운영체제가 회수하는것으로 알고 있습니다.
    종료후에도 회수 못하는 자원은 스트림 정도가 있을거 같네요

Leave a Reply

Your email address will not be published. Required fields are marked *