C 메모리 관리 - C memoli gwanli

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

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

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

1.1 C언어 메모리 구조

C 메모리 관리 - C memoli gwanli
출처: 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;
}
C 메모리 관리 - C memoli gwanli

그림과 같이 스택에 할당된 메모리들은 함수 호출이 종료되면 해제된다. 따라서 프로그래머가 별도로 관리하지 않아도 된다. 스택은 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 메모리 관리 - C memoli gwanli

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

www.kernelpanic.kr/33

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

www.kernelpanic.kr/34  

C 메모리 관리 - C memoli gwanli

메모리 구조

C 언어가 많은 사랑을 받는 이유는 바로 메모리가 아닐까 생각한다고 첫 시간에 이야기를 했었다.

오늘은 C 언어의 핵심 중 하나인 메모리 관리의 기본에 대해서 알아보도록 하겠다.

그 전에 먼저 메모리의 구조에 대해서 알아야하는데, 메모리 구조는 총 4개로 나뉘어 진다.

  1. 스택 영역
  2. 힙 영역
  3. 데이터 영역
  4. 코드 영역
C 메모리 관리 - C memoli gwanli

여기서 스택과 힙에 주목을 해볼 필요가 있다.

스택 영역

컴파일 시점에 메모리가 결정되어 실행 속도가 빠르고 낭비가 없다는 특징이 있다.

하지만 스택 영역은 상대적으로 한계가 있기 때문에 스택 이상을 쓰게된다면 에러가 발생할 가능성이 존재한다.

힙 영역

사용자가 직접 생성하는 혹은 할당하는 메모리 공간이다.

Java 에서는 GC가 동작하여 활약하는 공간이라고 생각할 수 있다.

힙은 최적화를 할 때도 사용되며 스택보다 큰 데이터를 할당할 때도 사용되긴 하지만 사용자가 직접 만질 수 있는 공간이기 때문에 아무래도 힙 손상이 발생하기 쉽다.

C 언어에서 메모리 사용하기

c 언어에서 메모리를 조작할 때는 malloc과 free 를 이용하는게 일반적이다.

  1. malloc 으로 메모리 할당하기
  2. 사용하기
  3. free 로 할당받은 메모리 해제하기

의 과정을 거치게 되는데, 좀 더 자세히 알아보자

malloc과 free

malloc과 free 와 같은 메모리 관련 함수는 stdlib.h 헤더에 존재하므로 malloc, free, memcpy, memset 을 사용하려면 해당 헤더를 include 해야 한다.

malloc

메모리를 할당받는 함수로 Memory + Allocation의 합성 약자로 메모리 공간을 확보하고 메모리 블록을 할당한다.

malloc 함수를 사용할 때 매개변수로 size 를 지정하여 사용자가 얼만큼의 메모리를 할당받을 것인지 명시해야 한다.

free

앞에서 본 malloc 으로 메모리를 할당했다면 Heap 손상을 발생시키지 않기 위해서 메모리를 해제시켜야 한다.

만약 메모리 할당을 해제하지 않으면 후속 할당에서 에러를 발생시킬 수 있으므로 적절하게 메모리를 해제시켜야 한다.

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

int main() {

    int number = 100;

    int *pointer1 = &number;
    int *pointer2 = malloc(sizeof(number));

    printf("address: %010x \n", pointer1);
    printf("address: %010x \n", pointer2);

    free(pointer2);
    return 0;
}

메모리에 값을 저장하기

앞선 코드에서는 메모리를 할당 하고 해제 하는 과정이었다.

이번에는 할당받은 메모리에 역참조를 이용하여 특정 값을 저장해보자.

#include <assert.h>
#include <stdlib.h>

int main() {

    int number = 100;

    int *pointer1 = &number;
    int *pointer2 = malloc(sizeof(number));

    *pointer2 = number;

    assert(*pointer1 == *pointer2);

    free(pointer2);
    return 0;
}