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

1. C언어 메모리 관리의 어려움 (1/2) - C언어 메모리 구조

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

포인터는 C언어의 가장 큰 강점이자 우리를 늘 메모리 관련 문제로 괴롭히는 큰 위험요인이다. 많은 좋은 프로그램들이 C언어의 포인터 기교를 사용해서 엄청난 메모리 / 성능상의 이득을 얻었다. 몇년도 전이지만, 아직도 리눅스 커널과 리눅스의 tcp/ip 스택에서 C언어 포인터를 활용한 것을 처음 봤을때의 신선한 충격이 생생하다. 훌륭한 프로그래밍 언어들이 많이 나왔지만 여전히 C의 포인터만큼 HW 직관적이고 강력한 개념은 없는 것 같다.

그러나 동시에 포인터는 원인을 찾기 어려운 버그들을 만들어 내곤 한다. 특히 어떤 버그들은 원인을 찾고서도 고치기가 몹시 난해한 경우들도 있다. 이는 훌륭한 프로그램에서도 예외가 아니다. 모질라 재단은 파이어폭스의 메모리 누수 문제로 수 년간 씨름하다가 결국 C++로 개발된 기존 브라우저 엔진(Gecko)을 버리고, 아예 새로운 언어로 엔진(Servo)을 다시 개발하였다. 도요타 자동차는 급발진으로 수 많은 인명사고를 냈는데, 조사 결과 이 역시 C언어의 메모리 관리 문제와 연관되어 있었다. (이 사건으로 오늘날 대부분의 자동차 회사들은 괴상하고 재미없지만 C언어의 메모리 관리 문제를 회피할 수 있는 misra-C를 내부적으로 사용하고 있다.)

C언어 메모리 문제들을 살펴보기 앞서 이번 포스트에서는 C언어의 메모리 구조와 C언어의 포인터로 인한 메모리 문제들의 유형들을 살펴볼 예정이다. 만약 C언어 메모리 문제들에 익숙하고 관리를 위한 테크닉들을 보고 싶으면 다음 포스트로 이동하자.

 

1.1 C언어 메모리 구조

 

출처: https://www.geeksforgeeks.org/memory-layout-of-c-program/

 

C언어 메모리 구조는 위 이미지와 같이 stack과 heap 그리고 static data 영역(initialized data)과 text 영역으로 구분된다. 각 영역은 다음과 같은 자료를 저장한다.

 

스택 (Stack)

스택은 이름처럼 해당 영역에 메모리를 블록처럼 쌓아놓았다고 생각하면 쉽다. (자료구조에서 스택을 안다면 이해하기 편할 것이다.) 블록이 차곡차곡 쌓여 있기 때문에 중간의 블록을 빼는 것은 안된다. 중간에 있는 블록을 빼려면 마지막에 들어온 블록부터 빼내야 한다.
스택 영역에는 지역변수들과 매개변수 및 리턴주소가 저장된다. 스택에 저장된 값들은 함수 호출이 종료되면 해제된다. 아래는 스택이 동작하는 간단한 예시이다.

#include <stdio.h>

void func2(int arg2_1, int arg2_2) {
    int local_var_2 = 3;

    printf("local_var_2=%d, arg2_1=%d, arg2_2=%d\n",
            local_var_2, arg2_1, arg2_2);
}

void func1(int arg1_1, int arg1_2) {
    int local_var_1_1 = 1;
    int local_var_1_2 = 2;

    printf("local_var_1_1=%d local_var_1_2=%d arg1_1=%d arg1_2=%d\n",
            local_var_1_1, local_var_1_2, arg1_1, arg1_2);

    func2(30, 40);
}

int main() {
    int main_local = 0;

    printf("main_local=%d\n", main_local);

    func1(10, 20);

    return 0;
}

그림과 같이 스택에 할당된 메모리들은 함수 호출이 종료되면 해제된다. 따라서 프로그래머가 별도로 관리하지 않아도 된다. 스택은 x86_64 아키텍쳐 리눅스 PC에서 약 8Mb의 크기를 가진다. (스택의 기본 크기는 아키텍쳐에 따라 달라진다.) 만약 스택이 8Mb의 크기보다 많이 설정되면, 스택 오버플로우 문제가 발생한다.

 

힙 (Heap)

힙은 '더미'로 직역된다. 스택이 함수 호출에 따라서 메모리가 차곡차곡 쌓인 모양으로 관리되는 반면, 힙은 이름 그대로 메모리가 힙 영역에 아무렇게나 할당되어 있다. 따라서 힙은 스택과 비교하면 크게는 다음 세가지 특징을 가지고 있다.

  1. 힙에 저장된 데이터는 함수 호출이 종료되어도 해제되지 않는다. 개발자가 명시적으로 해제하거나, 프로그램이 종료되어야 해제된다.
  2. 프로그램은 메모리 주소에 따라서 힙 데이터에 접근한다.
  3. 힙 공간은 크기의 제약이 없다. 메모리가 충분하다면, 힙 영역은 필요한 만큼 확장될 수 있다.

이와 같은 특징들로 인해서 힙은 스택과는 다른 종류의 메모리 문제를 발생시킨다.

  1. 사용이 종료된 데이터가 해제되지 않으면 프로그램이 종료될 때까지 메모리 공간을 차지한다.
  2. 프로그램은 힙에 데이터가 해제되었어도 메모리 주소에 따라서 접근할 수 있다.
  3. 힙 영역은 크기의 제한은 없지만 가용 메모리가 가득 찰 경우 커널의 OOM Killer에 의해 프로그램이 종료될 수 있다.

보통 힙에 메모리 할당은 malloc, calloc, realloc 함수를 사용하고, 해제는 free 함수를 사용한다. 아래는 힙에 메모리를 할당하는 코드와 간단한 견본 이미지이다.

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

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

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

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

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

    free(ex);

    return 0;
}

 

다음 포스트에서는 C언어 메모리 관리의 유형들에 대해 다룬다. (5/1 포스트 예정)

www.kernelpanic.kr/33

만약 바로 C언어 메모리 관리 방법에 대해서 알고 싶다면 아래 포스트를 참조하자. (5월 초 포스트 예정)

www.kernelpanic.kr/34  

반응형