Skip to content

OSTEP 14 Memory API

Published: at 오후 11:28

OSTEP 14 Memory API

1. 메모리 공간의 종류

C 프로그램이 실행되면, 두 가지 유형의 메모리 공간이 할당된다.

void func() {
    int x; // 스택에 int 형을 선언
    // rest of code
}
void func() {
	int *x = (int*) malloc(sizeof(int));
	// rest of code
}

한 행에 스택과 힙 할당이 모두 발생한다.

  1. 컴파일러가 포인터 변수의 선언 int *x 를 만나면 정수 포인터를 위한 공간을 할당해야 한다.
  2. 프로그램이 malloc() 을 호출하여 정수를 위한 공간을 힙으로부터 요구한다.
  3. 성공한 경우 그 정수의 주소를 반환, 실패한 경우 NULL을 반환

2. malloc() 함수

힙에 요청할 공간의 크기를 넘겨 주면, 성공했을 경우 새로 할당된 공간에 대한 포인터를 사용자에게 반환하고, 실패했을 경우 NULL을 반환하는 간단한 함수. malloc() 함수를 사용하기 위해 stdlib.h를 include 해야 한다.

malloc() 함수의 인자는 size_t 타입이고 이 변수는 필요 공간의 크기를 바이트 단위로 표시한 것이다. 숫자를 직접 쓰지 않고 sizeof() 함수를 통해 쓴다.

malloc() 함수는 void 타입에 대한 포인터를 반환한다. 주소만 넘겨주고 해당 주소에 어떤 타입의 자료를 저장할 지는 프로그래머가 결정하게 하는 전형적인 C 프로그래밍 방식이다.

3. free() 함수

더 이상 사용되지 않는 힙 메모리를 해제하기 위해 프로그래머는 free()를 호출한다.

int *x = malloc(10 * sizeof(int));
// rest of code
free(x);

한개의 인자 malloc()에 의해 반환된 포인터를 받는다. 할당된 영역의 크기는 전달되지 않는다. 그 크기는 메모리 할당 라이브러리가 알고 있어야 한다. 이 크기를 어떻게 알 수 있을까?

malloc()이나 calloc() 함수를 호출하여 동적 메모리를 할당하면, 실제로 요청된 크기보다 약간 더 많은 메모리가 할당됩니다. 이 추가된 메모리 영역에는 메타데이터 정보가 저장되며, 여기에는 할당된 메모리 블록의 크기, 사용 여부 등의 정보가 포함됩니다. 이러한 메타데이터는 메모리 블록이 해제되어야 할 때 free() 함수가 몇 바이트의 메모리를 해제해야 하는지 알 수 있게 해줍니다. 일반적으로 이 메타데이터는 할당된 메모리 블록 바로 앞에 위치하게 됩니다.

OSTEP 14 Memory API-1689612989858.jpeg

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

이 코드는 C 프로그래밍 언어의 malloc() 함수에서 메모리 할당에 사용되는 데이터 구조인 malloc_chunk의 정의입니다. malloc_chunk는 메모리 블록의 메타데이터를 저장하는 구조체로, 각 메모리 블록의 상태와 관련된 정보를 포함합니다. 구체적으로 다음과 같은 정보들이 저장됩니다:

  • prev_size: 이 필드는 현재 블록이 해제된 상태라면 바로 앞에 위치한 블록의 크기를 저장합니다. 현재 블록이 사용 중이면 이 필드는 무시됩니다.
  • size: 이 필드는 현재 메모리 블록의 전체 크기를 저장합니다. 이 크기에는 메타데이터의 크기도 포함되며, 또한 블록의 할당 상태에 대한 정보도 함께 저장됩니다. (보통 마지막 비트를 사용하여 블록이 사용 중인지 아닌지를 표시합니다)
  • fdbk: 이들 필드는 더블 링크드 리스트의 포워드(fd) 노드와 백워드(bk) 노드를 가리킵니다. 이들은 malloc_chunk가 메모리의 해제 리스트(free list)에 위치한 경우에만 사용됩니다. 이 리스트는 현재 사용되지 않고 해제된 상태의 메모리 블록들을 관리합니다.
  • fd_nextsizebk_nextsize: 이들 필드는 큰 메모리 블록의 경우에만 사용되며, 다음 크기의 더 큰 메모리 블록을 가리킵니다.

malloc() 함수가 호출되면, 메모리 블록 전체가 할당되고 메타데이터 영역에는 해당 블록의 크기 정보 등이 저장됩니다. 그런 다음 함수는 사용자 데이터가 저장될 영역의 시작 주소, 즉 메타데이터 바로 다음의 주소를 반환합니다. 이렇게 하면 free() 함수는 메모리 블록을 해제할 때 해당 블록의 크기를 알 수 있습니다. free() 함수가 호출되면, 해당 메모리 주소를 받아들이고 메타데이터 영역으로 “뒤로 걸어가” 메모리 블록의 크기를 확인한 후 그 크기만큼의 메모리를 해제합니다.

4. 흔한 오류

메모리 할당 잊어버리기

char *src = "hello";
char *dst;
strcpy(dst, src); // 할당이 되어 있지 않음. segfault.

Segmentation Fault는 프로그램이 자신이 접근 권한이 없는 메모리 영역에 접근하려고 시도할 때 발생하는 오류. 자세한 내용은 OSTEP 16 Segmentation에서 알아보자.

메모리를 부족하게 할당받기

char *src = "hello";
char *dst = malloc(strlen(src)); // 부족하게 할당
strcpy(dst, src);

Buffer Overflow. 구현 방식에 따라 정상적으로 작동할수도 있다. 프로그램이 한 번 올바르게 실행된다고 하더라고, 프로그램이 올바르다는 것을 의미하지는 않는다.

다음과 같이 작성해야 올바른 코드다.

char *src = "hello";
char *dst = malloc(strlen(src) + 1);
if (dst != NULL) {
    strcpy(dst, src);
}

할당받은 메모리 초기화하지 않기

malloc()을 호출해서 새로 할당받은 데이터 타입에 값을 넣어야 하는데 그냥 읽으면? 힙으로부터 알 수 없는 값을 읽는 일(uninitialized read)이 생긴다.

메모리 해제하지 않기

memory leak. 장시간 실행되는 응용 프로그램이나 운영체제 자체와 같은 시스템 프로그램에서 큰 문제다.

Garbage Collector가 있어도 이 문제를 피해갈 수 없다. 메모리 청크에 대한 참조가 존재하면, 어느 GC도 그 청크를 해제하지 않을 것이기 때문이다. 따라서 현대적인 언어에서도 메모리 누수는 여전히 문제가 된다.

한 바이트라도 명시적으로 할당받았으면 해제하는 습관을 들이도록 하자.

메모리 사용이 끝나기 전에 메모리 해제하기

dangling pointer. 심각한 실수이다. 차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다. free()를 호출하고 다른 용도로 malloc()을 호출하는 경우, 잘못 해제된 메모리를 재사용하게 된다.

반복적으로 메모리 해제하기

double free.

struct malloc_chunk* fd;         /* double links -- used only if free. */
struct malloc_chunk* bk;

chunk의 메타데이터 일부이다. 더블 링크드리스트를 만드는 데 쓰인다. 이 링크드리스트는 free()된 청크들을 모아놓는 리스트이다. 다음에 같은 사이즈의 할당이 될 경우 빠르게 재할당 할 수 있도록 한다. double free가 일어나면 이 링크드리스트에 같은 메모리 주소가 두 번 들어간다. 프로그래머의 의도와는 다르게, 변수 여러 개가 같은 메모리 공간을 점유하게 되어 버그가 발생한다.

free() 잘못 호출하기

malloc() 받은 포인터만 전달해야 한다. invalid frees는 매우 위험하다(undefined behavior).

5. 운영체제의 지원

malloc()free()는 시스템 콜이 아니라 라이브러리 함수이다. malloc()라이브러리는 시스템에게 더 많은 메모리를 요구하고 반환하는 시스템 콜(brk, sbrk)을 기반으로 구축된다.

brksbrk는 운영체제가 제공하는 시스템 호출입니다. 이들은 프로세스의 힙 영역에서 사용할 수 있는 메모리의 양을 관리합니다. brk는 프로그램의 힙 끝, 즉 “break” 위치를 설정하는 시스템 호출입니다. brk에 주어진 인자는 힙 영역의 새로운 끝을 지정합니다. 이 값이 현재 break보다 크다면, 운영체제는 힙 영역을 확장하고, 이 값이 현재 break보다 작다면, 운영체제는 힙 영역을 축소합니다. 다시 말해, brk는 운영체제에게 프로세스의 힙 영역 크기를 조정하도록 요청하는 함수입니다. brk와 비슷하게, sbrk도 힙 영역을 관리하는 시스템 호출입니다. 하지만 sbrk는 새로운 break 위치를 직접 지정하는 대신, 현재 break 위치에서의 상대적인 변화를 지정합니다. 즉, sbrk에 주어진 인자는 현재 break 위치에 더해질 크기를 나타냅니다. 이들 시스템 호출은 메모리 할당 라이브러리(예를 들어, mallocfree)가 운영체제로부터 메모리를 요청하거나 반환할 때 사용됩니다. 따라서 일반적으로는 프로그래머가 직접 사용하는 것이 아니라, 메모리 관리 라이브러리가 내부적으로 사용합니다. brksbrk를 프로그래머가 직접 사용하려고 하면, 메모리 할당 라이브러리의 내부 상태와 충돌이 발생할 수 있습니다. 이는 메모리 문제를 일으킬 수 있으므로, 프로그래머는 대신 mallocfree와 같은 라이브러리 함수를 사용하여 메모리를 할당하고 해제해야 합니다.

mmap() 함수를 통해 운영체제로부터 메모리를 얻는 방법도 있다. 특정 파일과 연결되지 않은 anonymous 메모리 영역을 생성한다. swap space에 연결되며, 힙과 유사하게 취급되고 관리된다.

6. 기타 함수들

calloc()

realloc()