[OSTEP] 14. 메모리 API (Memory API)

[OSTEP] 14. 메모리 API (Memory API)

안녕하세요, pingu52입니다.

이전 13장에서는 메모리 가상화의 개념인 주소 공간을 다루었습니다. 이번 14장은 실제 UNIX/C 프로그래밍 환경에서 메모리를 할당하고 관리하는 API와 주의사항을 다룹니다.

C 언어의 mallocfree 함수는 메모리 관리의 핵심이지만, 올바르게 사용하지 않을 경우 치명적인 오류를 발생시킵니다. 이번 글에서는 이들의 정확한 사용법과 흔한 오류 사례, 그리고 운영체제 차원의 지원 기능을 정리합니다.


1. 메모리의 종류: 스택(Stack) vs 힙(Heap)#

C 프로그램이 실행될 때 할당되는 메모리는 관리 방식에 따라 크게 두 가지로 나뉩니다.

1.1 스택 메모리 (Stack Memory)#

  • 관리 주체: 컴파일러 (암묵적 할당 및 해제)
  • 특징: 함수 호출 시 지역 변수 등을 저장하기 위해 자동으로 할당되며, 함수가 반환되면 자동으로 해제됩니다. 이러한 특성 때문에 **자동 메모리(Automatic Memory)**라고도 합니다.
  • 선언 예시:
void func() {
int x; // 스택에 정수형 변수 선언
// 함수 종료 시 x는 자동 해제됨
}

1.2 힙 메모리 (Heap Memory)#

  • 관리 주체: 프로그래머 (명시적 할당 및 해제)
  • 특징: 프로그램 실행 중 동적으로 크기가 결정되거나, 함수 반환 이후에도 데이터가 유지되어야 할 때 사용합니다. 프로그래머가 명시적으로 할당하고 해제해야 합니다.
  • 선언 예시:
void func() {
int *x = (int *) malloc(sizeof(int)); // 힙에 메모리 할당 요청
// ...
free(x); // 사용 후 반드시 해제
}

2. malloc() 함수#

힙 메모리를 할당할 때 사용하는 표준 라이브러리 함수입니다.

#include <stdlib.h>
void *malloc(size_t size);
  • 인자: 할당받을 메모리의 크기(바이트 단위).
  • 반환값: 성공 시 할당된 메모리의 시작 주소(void *), 실패 시 NULL.

사용 시 주의사항#

  1. sizeof() 연산자 사용: malloc(sizeof(int))와 같이 sizeof() 연산자를 사용하여 타입의 크기를 정확하게 전달해야 합니다. 숫자를 직접 입력하는 것은 이식성을 떨어뜨립니다.
  2. 문자열 할당: 문자열을 위한 공간을 할당할 때는 반드시 종료 문자(\0)를 고려하여 malloc(strlen(s) + 1)과 같이 1바이트를 더 할당해야 합니다. sizeof()는 포인터 자체의 크기만 반환하므로 문자열 길이를 구할 때는 적합하지 않습니다.

3. free() 함수#

더 이상 사용하지 않는 힙 메모리를 운영체제에 반환하는 함수입니다.

void free(void *ptr);
  • 인자: malloc()이 반환했던 포인터.
  • 특징: 해제할 메모리의 크기를 인자로 전달하지 않습니다. 메모리 할당 라이브러리가 내부적으로 해당 포인터가 가리키는 메모리 블록의 크기를 추적하고 있기 때문입니다.

4. 흔한 오류 (Common Errors)#

메모리 관리는 오류가 발생하기 쉽습니다. 다음은 컴파일은 성공하지만 실행 시 문제를 일으키는 대표적인 오류 유형입니다.

4.1 메모리 할당 누락 (Forgetting To Allocate Memory)#

포인터를 선언한 후 메모리를 할당하지 않고 데이터를 복사하려는 경우입니다. 이 경우 Segmentation Fault가 발생합니다.

char *src = "hello";
char *dst; // 메모리 할당되지 않음
strcpy(dst, src); // Segfault 발생

4.2 메모리 공간 부족 (Not Allocating Enough Memory)#

필요한 크기보다 적게 메모리를 할당하는 경우로, 버퍼 오버플로우(Buffer Overflow) 라고 합니다. 주로 문자열 종료 문자 공간을 계산하지 않았을 때 발생합니다.

char *src = "hello";
char *dst = (char *) malloc(strlen(src)); // 종료 문자 공간 부족
strcpy(dst, src); // 힙 영역 침범 발생

4.3 초기화 누락 (Forgetting to Initialize)#

malloc()은 메모리 공간만 확보할 뿐 데이터를 초기화하지 않습니다. 초기화하지 않은 메모리를 읽을 경우 예측할 수 없는 값(Garbage Value)을 읽게 됩니다(Uninitialized Read).

4.4 메모리 해제 누락 (Memory Leak)#

할당한 메모리를 free()로 해제하지 않는 경우 **메모리 누수(Memory Leak)**가 발생합니다. 장시간 실행되는 서버 프로그램 등에서 메모리 누수가 누적되면 시스템 메모리가 고갈될 수 있습니다.

NOTE

참고: 프로세스가 종료되면 운영체제가 해당 프로세스의 모든 메모리를 회수합니다. 따라서 단기 실행 프로그램에서는 치명적이지 않을 수 있으나, 항상 메모리를 해제하는 습관을 갖는 것이 권장됩니다.

4.5 해제된 메모리 사용 (Dangling Pointer)#

이미 free()를 통해 반환된 메모리 주소에 접근하는 오류입니다. 프로그램이 충돌하거나, 해당 주소에 다른 데이터가 할당되었을 경우 데이터 무결성이 훼손될 수 있습니다.

4.6 중복 해제 (Double Free)#

동일한 포인터에 대해 free()를 두 번 이상 호출하는 오류입니다. 메모리 할당 라이브러리의 관리 구조가 손상되어 크래시가 발생할 수 있습니다.


5. 운영체제의 지원 (Underlying OS Support)#

mallocfree는 시스템 콜이 아닌 C 라이브러리 함수입니다. 이 라이브러리는 운영체제의 시스템 콜을 사용하여 메모리를 관리합니다.

5.1 brk / sbrk#

프로그램의 힙 영역 끝(Program Break) 위치를 변경하는 시스템 콜입니다. sbrk를 호출하여 힙의 크기를 늘리거나 줄일 수 있습니다. 현대 프로그래밍에서는 이를 직접 호출하지 않고 malloc을 사용합니다.

5.2 mmap#

mmap() 시스템 콜을 사용하여 운영체제로부터 익명 메모리 페이지(Anonymous Memory Region)를 직접 할당받을 수 있습니다. malloc 구현체는 대용량 메모리를 할당할 때 brk 대신 mmap을 사용하기도 합니다.


6. 기타 함수#

  • calloc(): 메모리를 할당함과 동시에 모든 비트를 0으로 초기화합니다.
  • realloc(): 이미 할당된 메모리 블록의 크기를 변경합니다. 기존 데이터를 유지하며 더 큰 공간으로 확장하거나 축소할 때 사용합니다.

7. 요약 (Summary)#

이번 장에서는 메모리 API와 관리 기법을 정리했습니다.

  • 스택은 컴파일러가 관리하는 자동 메모리이며, 은 프로그래머가 관리하는 수동 메모리입니다.
  • malloc으로 메모리를 할당하고 free로 해제합니다.
  • 메모리 누수, 버퍼 오버플로우, 초기화 누락 등의 오류는 프로그램의 안정성을 저해합니다.
  • gdbvalgrind 같은 디버깅 및 분석 도구를 활용하여 메모리 관련 오류를 탐지할 수 있습니다.

다음 장부터는 운영체제가 이 가상 주소 공간을 물리 메모리에 어떻게 매핑하는지, 구체적인 주소 변환 메커니즘을 다룹니다.


8. 용어 정리#

  • 스택 메모리(Stack Memory): 함수 호출 시 자동 할당/해제되는 메모리 영역.
  • 힙 메모리(Heap Memory): malloc/free를 통해 명시적으로 관리되는 동적 메모리 영역.
  • Segmentation Fault: 프로그램이 접근 권한이 없는 메모리 영역에 접근 시 발생하는 오류.
  • Buffer Overflow: 할당된 메모리 범위를 벗어나 데이터를 기록하는 오류.
  • Memory Leak: 할당된 메모리를 해제하지 않아 사용할 수 없는 메모리가 누적되는 현상.
  • Dangling Pointer: 해제된 메모리를 가리키는 포인터.
  • brk/sbrk: 프로세스의 데이터 세그먼트 크기를 조정하는 시스템 콜.

Reference#