콘텐츠로 이동

핵심 질문: 어떻게 메모리를 할당하고 관리해야 하는가

Unix/C 프로그램에서 메모리를 할당하고 관리하는 방법을 이해하기
→ 강력하고 안정적인 소프트웨어를 구축하는 데 중요하다.

  1. 일반적으로 어떤 인터페이스가 사용되는가?
  2. 어떤 실수를 해서는 안 되는가?

목차

  1. malloc / free
  2. calloc / realloc
  3. brk / sbrk
  4. mmap / mumap

메모리 공간의 종류

Virtual Address Space

Pasted image 20230212144334.png

Pasted image 20230212131349.png

자료 출처 1

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

  1. 스택 (stack) 메모리
할당과 반환은 프로그래머를 위해 컴파일러에 의해 암묵적으로 이루어진다.
이러한 이유 때문에 때로는 자동(automatic) 메모리라고 불린다.

func()라는 함수 안에서 x라 불리는 정수를 위한 공간이 필요하다고 하자.
이러한 메모리를 선언하려면 다음과 같이 하면 된다.

C
void func() {  
int x; //스택에 int 형을 선언  
}  

컴파일러가 나머지 작업을 수행하여, func()가 호출될 때 스택에 공간을 확보한다.

함수에서 리턴하면 컴파일러는 프로그래머 대신에 메모리를 반환한다. 함수 리턴 이후 에도 유지되어야 하는 정보는 스택에 저장하지 않는 것이 좋다.

  1. 힙(heap) 메모리
    오랫동안 값이 유지되어야 하는 변수 저장
    모든 할당과 반환이 프로그래머에 의해 명시적으로 처리된다.

다음 코드는 정수에 대한 포인터를 힙에 할당하는 예를 보여 준다.

C
void func() {  
int *x = (int *) malloc(sizeof(int));  
. . .  
}  

이 코드에서는 한 행에 스택 할당이 모두 발생

  1. 컴파일러가 포인터 변수의 선언 (int *x)을 만나면 정수 포인터를 위한 공간을 할당해야 한다는 것을 안다.
  2. 프로그램이 malloc()을 호출하여 정수를 위한 공간을 힙으로부터 요구한다.

이 반환된 주소는 스택에 저장되어 프로그램에 의해 사용된다.

malloc()

man malloc 이라고 터미널에 쳐보면 다음과 같은 정보가 나온다.

Pasted image 20230212152238.png
명령어 라인에 man malloc이라고 입력하면 이런 정보를 볼 수 있다.

C
#include <stdlib.h>  
. . .  
void *malloc(size_t size);  

모든 C 프로그램에 디폴트로 링크되는 C 라이브러리는 malloc() 함수를 가지고 있다.
헤더 파일을 추가하면 malloc()을 올바르게 호출했는지 컴파일러가 검사할 수 있다.
전달된 인자의 개수가 맞는지 올바른 데이터 타입의 인자를 전달했는지 검사하게 된다.

  • malloc()
    malloc()은 void 타입에 대한 포인터를 반환한다. 힙에 요청할 공간의 크기를 넘겨 준다.
    성공: 새로 할당된 공간에 대한 포인터를 사용자에게 반환
    실패: NULL을 반환

  • malloc()의 인자

    • size_t size: 필요 공간의 크기를 바이트 단위로 표시한 것
    • size_t (unsigned integer type)

sizeof()

Usage of sizeof() Operator

sizeof() operator is used in different ways according to the operand type.
프로그래머는 타입 변환(type casting) 을 이용하여 공간 활용을 결정한다.

  • malloc() 호출에서는 정확한 크기의 공간을 요청하기 위하여 sizeof() 연산자를 사용
  • C 언어에서 sizeof()는 통상 컴파일 시간 연산자이다.
  • 인자의 실제 크기가 컴파일 시간에 결정된다.
  • sizeof()는 연산자로 간주되는 게 맞으며 함수 호출이 아니다 (함수 호출은 실행 시간에 일어남).
  • 데이터 타입뿐 아니라 변수의 이름도 sizeof()의 인자로 전달할 수 있다.
  • 동적 배열 선언일경우에만 sizeof() 가 런타임에 실행되고 그 외에는 컴파일에서 실행되는 시간 연산자다
    • 전자는 동적 배열이라서 배열 메모리를 힙에다 넣음
    • 정적 배열인경우에는 이미 배열 메모리를 컴파일 시점에 스택에 넣음
  1. When the operand is a Data Type
    When sizeof() is used with the data types such as int, float, char… etc it simply returns the amount of memory allocated to that data types.

Sizeof is a much-used operator in the C.
It is a compile-time unary operator which can be used to compute the size of its operand.

The result of sizeof is of the unsigned integral type which is usually denoted by size_t.

sizeof can be applied to any data type, including primitive types such as integer and floating-point types, pointer types, or compound datatypes such as Structure, union, etc.

  1. To allocate a block of memory dynamically: sizeof is greatly used in dynamic memory allocation.

For example, if we want to allocate memory that is sufficient to hold 10 integers and we don’t know the sizeof(int) in that particular machine. We can allocate with the help of sizeof.

C
double *d = (double *) malloc(sizeof(double));  
int* ptr = (int*)malloc(10 * sizeof(int));  

sizeof()는 숫자 (이 경우 double의 크기인 8)로 대체되어 malloc()에 전달된다.

  • sizeof가 runtime에 실행되는 경우

In almost all cases, sizeof is evaluated based on static type information (at compile-time, basically).

One exception (the only one, I think) is in the case of C99’s variable-length arrays (VLAs).

However, when applied to a variable length array, it is evaluated at runtime.

Otherwise the operand expression is not evaluated at all, and only the type of the expression is used to determine the result.

  1. The actual size of ‘x’ is known at run-time
    C
    int *x = malloc(10 * sizeof(int));  // 정수형 원소 10개를 가지는 배열을 위한 공간을 선언  
    printf(%d\n , sizeof(x));   
    
메모리 없을 때 문제가 생기니까 정적으로 할당

Result

Bash
4 # (32 bit 컴퓨터인 경우)  
8 # (64 bit 컴퓨터인 경우)  

As of C99, sizeof is evaluated at runtime if and only if the operand is a variable-length array, e.g. int a[b], where b is not known at compile time. In this case, sizeof(a) is evaluated at runtime and its result is the size (in bytes) of the entire array, i.e. the size of all elements in the array, combined. To get the number of elements in the array, use sizeof(a) / sizeof(b). From the C99 standard:

The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type.
The size is determined from the type of the operand. The result is an integer.
If the type of the operand is a variable length array type, the operand is evaluated; otherwise, the operand is not evaluated and the result is an integer constant.

Note that all of this is different from what you’d get if you allocated an array on the heap, e.g. int* a = new int[b].

In that case, sizeof(a) would just give you the size of a pointer to int, i.e. 4 or 8 bytes, regardless of how many elements are in the array.

sizeof()는 동적으로 할당받은 메모리의 크기가 얼마인지가 아니라, 정수를 가리키는 포인터의 크기가 얼마인지 물어본다고 생각함

Note that all of this is different from what you’d get if you allocated an array on the heap, e.g. int* a = new int[b]. In that case, sizeof(a) would just give you the size of a pointer to int, i.e. 4 or 8 bytes, regardless of how many elements are in the array.

The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type. The size is determined from the type of the operand. The result is an integer. If the type of the operand is a variable length array type, the operand is evaluated; otherwise, the operand is not evaluated and the result is an integer constant.

The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type.
The size is determined from the type of the operand. The result is an integer.
If the type of the operand is a variable length array type, the operand is evaluated; otherwise, the operand is not evaluated and the result is an integer constant.

  1. The actual size of ‘x’ is known at compile-time
    C
    int x[10];  
    printf(%d\n , sizeof(x));   
    
Result
Bash
40 # 컴파일러가 변수 x에 40바이트 할당됨 (정적인 정보)  
추가 참고 사이트

문자열에서 sizeof()

문자열을 위한 공간을 선언 할 때 strlen() 함수로 문자열의 길이를 얻어낸 뒤 문자열-끝을 나타내는 문자를 위한 공간을 확보하기 위해 1바이트를 더해야 한다.

C
malloc(strlen(s) + 1)  

Memory API: free()

더 이상 사용 되지 않는 힙 메모리를 해제

C
int *x = malloc(10 * sizeof(int));  
. . .  
free(x);  

한 개의 인자, malloc()에 의해 반환된 포인터를 받는다.
할당된 영역의 크기는 전달되지 않는다.

할당된 메모리의 크기는 메모리 할당 라이브러리가 알고 있어야 한다.

C
#include <stdlib.h>  

void free(void* ptr)  
  • Argument
    void *ptr: a pointer to a memory block allocated with malloc
    return: none

Memory Allocating

Pasted image 20230212155627.png

Memory Freeing

Pasted image 20230212155733.png


메모리 관리 관련 자주 저지르는 실수들

malloc()과 free()를 사용하는 데 흔히 발생하는 오류들

모든 예들은 컴파일러 최적화 없이 컴파일되고 실행되었다.
C 프로그램을 컴파일하는 것은 제대로 돌아가는 C 프로그램을 제작하는 데 반드시 필요하지만, 그것만으로는 충분하지 않다는 것을 배우게 될 것이다.

올바른 메모리 관리가 그렇다. 많은 새로운 언어들이 자동 메모리 관리(automatic memory management)를 지원한다.

그러한 언어들에서는 메모리를 할당하기 위하여 malloc()과 유사한, 보통 new 또는 새 객체를 할당하기 위한 비슷한 루틴을 호출하는 반면에, 공간을 해제하기 위해서는 아무것도 호출하지 않는다.

쓰레기 수집기 (garbage collector)가 실행되어 참조되지 않는 메모리가 찾아 알아서 해제한다.

- Forgetting To Allocate Memory

메모리 할당 잊어버리기
많은 루틴은 자신이 호출되기 전에 필요한 메모리가 미리 할당되었다고 가정한다.
예를 들어, strcpy(dst, src) 루틴은 소스 포인터에서 목적 포인터로 문자열을 복사한다.
  • Incorrect Code
    C
    char *src =hello ; // character string constant  
    char *dst; // unallocated  
    strcpy(dst, src); // segfault & 죽음  
    

    Pasted image 20230212161601.png

이 코드를 실행하면 세그멘테이션 폴트(segmentation fault)가 발생할 가능성이 높다. ~ 메모리 관련 에러

  • Correct Code

C
char *src =hello ; // character string content  
char *dst = (char *) malloc(strlen(src) + 1); // allocated  
strcpy(dst, src); // work properly  

Pasted image 20230212161644.png

대안으로, strdup()을 사용하여 훨씬 편하게 할 수 있다.

- Not Allocating Enough Memory

메모리를 부족하게 할당받기
이 오류는 메모리를 부족하게 할당받는 것으로, 때때로 버퍼 오버플로우(bufer overlow)라고 불린다.
목적지 버퍼 공간을 약간 부족하게 할당받는다.
  • Incorrect Code, But Work Properly
    C
    char *src =hello  // character string constant  
    char *dst = (char *) malloc(strlen(src)); // too small  
    strcpy(dst, src); // work properly  
    

Pasted image 20230212162837.png

malloc이 구현 방식과 기타 많은 세부 사항에 따라서는 이 프로그램이 제대로 동작하는 것처럼 보이는 경우가 종종 있다.

어떤 경우에는 문자열 복사가 실행될 때 할당된 공간의 마지막을 지나쳐 한 바이트 만큼 더 공간을 사용한다.

이 공간이 더 이상 사용되지 않는 변수 영역이기 때문에 덮어쓰더라도 아무 피해가 발생하지 않을 때도 있다. 다른 때에는 이러한 오버플로우가 매우 유해할 수 있고 사실 많은 시스템에서 보안 취약점의 원인이다. 어떤 경우에는 malloc 함수 라이브러리가 여분의 공간을 할당하고 프로그램은 다른 변수의 값을 덮어쓰지 않고 잘 동작한다. 또 다른 경우에는 프로그램은 고장을 일으키고 크래시된다. 다음과 같은 또다른 중요한 교훈을 얻는다 : 프로그램이 한 번 올바르게 실행된다고 하더라도, 프로그램이 올바르다는 것을 의미하지는 않는다.

- Forgetting to Initialize

할당받은 메모리 초기화하지 않기
malloc()을 제대로 호출했지만 새로 할당받은 데이터 타입에 특정 값을 넣는 것을 잊는 경우

C
int *x (int *)malloc(sizeof(int)); // allocated  
printf("*x = %d\n", *x); // uninitialized memory access  

Pasted image 20230212163721.png

초기화하지 않는다면, 프로그램은 결국 초기화되지 않은 읽기(uninitialized read), 즉 힙으로부터 알 수 없는 값을 읽는 일이 생긴다.

- Memory Leak

  • 메모리 해제하지 않았을 때 발생

  • A program keeps allocating memory without freeing it.

  • A program runs out of memory and eventually is killed by OS.
C
while(1)  
    malloc(4);  

Pasted image 20230212155944.png

장시간 실행되는 응용 프로그램이나 또는 운영체제 자체와 같은 시스템 프로그램 에서는 큰 문제이다.
메모리가 천천히 누수되면 결국 메모리가 부족하게 되고 시스템을 재시작해야함.

메모리 청크의 사용이 끝나면 반드시 해제해야 한다. 쓰레기 수집 기능이 있는 언어도 이 문제에는 도움이 되지 않는다.

메모리 청크에 대한 참조가 존재하면, 어느 쓰레기 수집기도 그 청크를 해제하지 않을 것이고, 따라서 이런 현대적인 언어에서도 메모리 누수는 여전히 문제가 된다. 경우에 따라서 free()를 호출하지 않는 것이 타당하게 보이는 경우도 있다.

프로세스를 종료하면 메모리 누수가 일어나지 않는 이유

실행 시간이 짧은 프로그램을 작성할 때 malloc()을 이용하여 일부 공간을 할당할 수 있다.
프로그램은 실행 후 곧 종료할 것이다. 종료 직전에 free()를 호출할 필요가 있을까?
호출하지 않으면 잘못한 것처럼 보이지만 실제로는 손해보지 않는다. 이유는 간단하다.
시스템이 실제 두 단계로 메모리를 관리하기 때문이다.
메모리 관리 단계
1단계) 운영체제에 의해 수행
프로세스가 실행할 때 메모리를 프로세스에게 건네 주고 프로세스가 종료하거나 다른 이유로 죽을 때 메모리를 되돌려 받는다.

2단계) 각 프로세스 내에서, 예를 들면, malloc()과 free()를 호출할 때 힙 내에서 수행된다.
free()를 호출하지 못하여 메모리가 누수되었더라도 프로세스가 종료할 때, 운영체제는 프로세스의 모든 메모리를 회수한다.
프로세스의 코드, 스택, 및 여기서 논의 중인 힙 등 모든 페이지가 포함된다.
프로세스 주소 공간의 힙이 어떤 상태가 되더라도, 운영체제는 프로세스가 죽을 때 그 모든 페이지를 회수한다.
직접 해제하지 않았더라도 메모리가 누수되지 않는다.
따라서 동작이 짧은 응용 프로그램 같은 경우에는 괜찮지만, Web 서버나 데이터베이스 관리 시스템 등의 결코 종료하지 않는 장시간 실행되는 서버 (웹서버나 데이터베이스 관리 시스템과 같이 절대 종료하지 않는 것)의 코드를 작성할 때, 누수 메모리는 훨씬 심각한 문제이고 응용 프로그램이 메모리를 다 소진하게 되면 결국 크래시가 발생한다.

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

→ Dangling Pointer

Pasted image 20230212160001.png

메모리 사용이 끝나기 전에 메모리 해제하기 때때로 프로그램은 메모리 사용이 끝나기 전에 메모리를 해제한다.
차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다.

예를 들어, free()를 호출하고, 그 후 다른 용도로 malloc()을 호출하면 잘못 해제된 메모리를 재사용한다.

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

프로그램은 가끔씩 메모리를 한 번 이상 해제하며 이중 해제(double free)라 불린다.
메모리 할당 라이브러리에서 크래시와 같은 오류가 난다.

- free() 잘못 호출하기

free()는 malloc() 받은 포인터만 전달될 것으로 예상한다. 그 이외의 값을 전달하면 문제가 발생한다.

유효하지 않은 해제(invalid frees)는 매우 위험하고 당연히 피해야 한다.

  • Free the memory that was freed already.
    C
    int *x (int *)malloc(sizeof(int)); // allocated  
    free(x); // free memory  
    free(x); // free repeatedly  
    

Pasted image 20230212160023.png

  • Free memory that was not allocated via malloc();
    C
    int *x (int *)malloc(sizeof(int)); // allocated  
    free(x + 12); // free memory  
    

메모리 관련 문제의 원인 찾는 도구: purify, valgrind


Other Memory APIs: calloc(), realloc()

  • calloc(): 메모리 할당 영역을 0으로 채워서 반환 → 초기화하는 것을 잊어버리는 오류를 방지
    C
    #include <stdlib.h>  
    void *calloc(size_t num, size_t size)  
    
  • size_t num: the number of objects to allocate
  • size_t size: size of an object (in bytes)

  • realloc(): realloc()은 더 큰 새로운 영역을 확보하고 옛 영역의 내용을 복사한 후에 새 영역에 대한 포인터를 반환
    이미 할당된 공간에 대해 (eg., 배열) 추가의 공간이 필요할 때 사용 Change the size of memory block.

    C
    #include <stdlib.h>  
    void *realloc(void *ptr, size_t size)  
    

  • void *ptr: Pointer to memory block allocated with malloc, calloc or realloc
  • size_t size: New size for the memory block (in bytes)

System Calls

malloc(), free() → 라이브러리 함수 (시스템 콜 X)

malloc 라이브러리: 프로세스 가상 주소 공간 안의 공간을 관리
라이브러리: 운영체제 시스템 콜을 기반으로 구축

  1. brk 시스템 콜
    프로그램의 break 위치를 변경하는 데 사용된다.
    break는 힙의 마지막 위치를 나타낸다. brk는 새로운 break 주소를 나타내는 한 개의 인자를 받는다.

새로운 break가 현재 break보다 큰지 작은 지에 따라 힙의 크기를 증가시키거나 감소시킨다.

  1. sbrk는 증가량만을 받아들이는 것을 제외하고 비슷한 용도로 사용된다.

  2. 반드시 malloc()과 free()를 사용하기

    • brk 또는 sbrk를 직접 호출해서는 안 된다.
    • 이들은 메모리 할당 라이브러리에 의해 사용된다.
C
#include <unistd.h>  

int brk(void *addr);  
void *sbrk(intprt_t, increment);  
  • There lacks of heap space → Ask OS to expand heap.
    • break: The location of the end of the heap in address space.
    • malloc uses brk system call.
      • brk is called to expand the program’s break.
      • sbrk is similar to brk.
      • Programmers should never directly call either brk or sbrk

Pasted image 20230212160053.png

mmap()

추가 참고 페이지: mmap()–Memory Map a File - IBM Documentation

운영체제로부터 메모리를 얻는 함수
올바른 인자를 전달하면 mmap()은 프로그램에 anonymous의 메모리 영역을 만듬.

anonymous 영역은 특정 파일과 연결되어 있지 않고 가상 메모리 스왑 공간(swap space)에 연결된 영역을 말한다.
이 메모리는 힙처럼 취급되고 관리된다.

The mmap() function establishes a mapping between a process’ address space and a stream file.
The address space of the process from the address returned to the caller, for a length of len, is mapped onto a stream file starting at offset off.

The portion of the stream file being mapped is from starting offset off for a length of len bytes. The actual address returned from the function is derived from the values of flags and the value specified for address.

The mmap() function causes a reference to be associated with the file represented by fildes.

This reference is not removed by subsequent close operations. The file remains referenced as long as a mapping exists over the file.

- Creating File-backed Region

C
# include <sys/mman.h>  
void *mmap(void *ptr, size_t length, int prot, int flags, int fd, off_t offset)  
  • Allocate a memory region of length at ptr.
  • If fd is not negative, associate the region to fd starting at offset.

Pasted image 20230212160117.png

- Creating Anonymous Region

C
# include <sys/mman.h>  
void *mmap(void *ptr, size_t length, int prot, int flags, int fd, off_t offset)  

C
ptr = mmap(NULL, 40, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUSE, -1, 0);  

if (ptr == MAP_FAILED)  
    exit (EXIT_FAILURE);  

Pasted image 20230212160643.png


마지막 업데이트 : 2025년 4월 23일
작성일 : 2023년 4월 2일