본문 바로가기
모던 C 언어/C언어 메모리관리

1. C언어 메모리 관리의 어려움 (2/2) - C언어 메모리 문제 유형들

by 커널패닉 2021. 4. 28.
반응형

본 포스트에서는 C언어에서 쉽게 겪을 수 있는 메모리 문제들의 유형을 다룬다. 만약 C언어 메모리 구조에 대해서 잘 알지 못한다면, 앞선 포스트(www.kernelpanic.kr/32)를 먼저 보고오자. C언어 메모리 구조 및 어떤 종류의 메모리 문제 유형들이 있는지 잘 안다면 다음 포스트(www.kernelpanic.kr/34)로 넘어가면 된다.

이번 주제에서 C의 모든 메모리 문제를 해결할 수 있는 솔루션을 제시하지는 않는다. 그럴수도 없다. 다만 메모리 릭과 댕글링 포인터 이슈를 회피하는 테크닉에 대해서 다룰 예정이다. 따라서 메모리 릭과 댕글링 포인터 문제 유형에 대해서 확인하고 가면 된다.

 

1.2.1 메모리 릭(Memory Leak)

메모리 릭은 프로그램이 불필요한 메모리를 계속 점유하는 현상이다. 일반적으로 프로그램 가동 시간 혹은 사용량에 비례해서 프로그램의 메모리 사용량이 점점 늘어나는 것으로 확인할 수 있다. 아래 그림의 파란색 그래프(SVG patch off)가 메모리 릭이 발생한 모습이다. 사용 시간에 따라서 점진적으로 메모리 사용량이 증가하는 모습을 관찰할 수 있다. 반면 메모리 릭이 발생하지 않은노란색 그래프(SVG patch on)는 오르락 내리락 하면서 일정한 메모리 사용량을 보인다.

출처: https://www.researchgate.net/figure/An-Example-of-Memory-Leak-Detection-and-Its-Result_fig1_321894773

메모리 릭은 힙 영역에 할당된 메모리를 해제하지 않아서 발생한다. C언어에서는 힙 영역에 동적 메모리를 할당한다. 그런데 힙 영역에 할달된 메모리는 함수호출이 종료되어도 해제되지 않는다. 대신 free() 등을 이용해서 사용이 완료된 메모리를 명시적으로 해제해야 한다. 그러나 이 메모리가 해제되지 않으면 메모리 릭이 발생한다.

 

1.2.2 댕글링 포인터 (Dangling Pointer)

댕글링 포인터(또는 허상 포인터)는 이미 해제된 메모리를 가르키는 포인터를 뜻한다. 동적할당된 메모리를 가리키는 포인터가 있다고 생각해보자. 포인터는 메모리가 동적할당된 공간(힙)에 정상적으로 있다고 알고 있다. 만약 여기서 동적할당된 메모리가 해제되면 해당하는 공간을 가리키던 포인터에 있는 주소 공간은 아무런 의미가 없게 된다. 이 때 포인터에 저장된 주소공간에서 데이터를 가져오거나 어떤 동작을 수행하면 문제가 될 수 있다. 심지어 경우에 따라서는 당장은 쓰레기 값이 넘어올 뿐 정상동작하는 것처럼 보이다가 예상치 못한 경우에 크래쉬를 일으킬 수 도 있다. 이러면 디버깅을 하기 아주 골치아픈 상황이 된다. 아래는 의도적으로 댕글링 포인터를 생성하고 해당 댕글링 포인터의 값을 호출하는 코드이다.

#include <stdio.h>
#include <stdlib.h>

struct example {
    int a;
    int b;
    char c;
};

int main() {
    struct example *ex;
    struct example *cloned;
    ex = malloc(sizeof(*ex));
    cloned = ex;

    ex->a = 1;
    ex->b = 2;
    ex->c = 'c';

    printf("[origin] a=%d b=%d c=%c\n", ex->a, ex->b, ex->c);
    printf("[cloned] a=%d b=%d c=%c\n", 
            cloned->a, cloned->b, cloned->c);

    free(ex);
    ex = NULL;

    /*
     * 아래 라인은 Segfault를 일으킨다.
     */
    // printf("[origin] a=%d b=%d c=%c\n", ex->a, ex->b, ex->c);
    
    /*
     * 아래 라인에서 dangling pointer로 인한 이상한 값이 출력되었다.
     */
    printf("[dangling] a=%d b=%d c=%x\n", 
            cloned->a, cloned->b, cloned->c);

    return 0;
}

컴파일 후 실행하면 아래와 같이 동작은 문제가 없다. 대신 free가 이뤄진 후 출력되는 정보에는 쓰레기 값이 들어 있음을 확인할 수 있다. (쓰레기 값은 실행하는 환경에 따라 달라진다.)

[origin] a=1 b=2 c=c
[cloned] a=1 b=2 c=c
[dangling] a=1633718123 b=5 c=10

 

1.2.3 그 외

이 외에도 대표적인 메모리 관련 버그는 아래가 있다. 다만 아래 문제들에 대한 해결 방법에 대해서는 다루지 않을 예정이기 때문에 간단히 이런게 있다 정도만 말하고 넘어가려 한다.

 

버퍼 오버플로우 (Buffer Overflow)

버퍼 오버플로우는 배열 혹은 동적으로 할당된 메모리 영역을 벗어나서 쓰기 작업을 수행하는 것이다.

 

널포인터 익셉션(Null Pointer Exception)

Null로 초기화한 메모리 영역에 접근하면 발생하는 에러이다. 위 예제코드에서 주석 부분이 널포인터 익셉션에 해당한다.

 

반응형