티스토리 뷰

반응형

안녕하세요 Pingu입니다.

지난 글에서 간단하게 메모리 가상화에 대해 알아보았는데요, 이번 글에서는 메모리를 사용하기 위한 Memory API들을 살펴보려고 합니다. 제가 공부할 때 참고하고 있는 OSTEP 책에서는 Chapter 14 - Memory API부분 입니다.

Interlude: Memory API

이번 글에서는 UNIX 시스템에서 메모리 할당 인터페이스에 대해 알아보겠습니다! 제공되는 인터페이스는 몇 개 없기 때문에 빠르게 알아보겠습니다. 우선 이번 글에서 알아볼 인터페이스로 처리할 문제는 메모리 할당 및 관리 방법입니다. 또한 이러한 인터페이스를 사용할 때 실수할 수 있는 부분에 대해서도 알아보겠습니다.

Types of Memory

실행할 때 메모리의 상태에 대해 기억하시나요? 기억이 나지 않는 분들을 위해 그림을 첨부해보면 아래와 같습니다!

위와 같은 구조에서 data, text 부분은 고정된 부분입니다. 즉 프로그램을 실행하더라도 이 부분이 변하는 일은 없습니다. 이번 글에서 주의 깊게 볼 부분들은 stack, heap 구간이며 특히 heap을 잘 봐야 합니다. Stack 메모리는 개발자가 따로 처리할 일은 없습니다. 따라서 이를 자동 메모리라고도 합니다. Stack을 사용하는 방법은 아주 간단합니다. 위에 적힌 대로 지역변수를 하나 선언하면 됩니다. 코드로 예를 들자면 아래와 같습니다.

void func() {
    int x;
}

위와 같이 코딩하면 func()가 호출될 때 x를 위해 스택에 메모리 공간을 확보합니다. 그런 뒤 func()의 호출이 끝나면 알아서 메모리에서 해제됩니다. 즉 개발자가 메모리를 할당, 해제할 필요가 없습니다. 그렇다면 heap은 어떨까요? 위의 그림에서 볼 수 있듯 heap은 동적 할당 메모리입니다. 즉 개발자가 메모리를 할당, 해제를 해줘야 한다는 것이죠! 이러한 특징 때문에 많은 실수가 발생하는 원인이기도 합니다. 그럼 간단하게 heap에 메모리를 할당하는 코드를 보겠습니다.

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

위와 같이 개발자가 직접 메모리의 크기를 정해서 변수에 할당해줘야 heap을 사용할 수 있습니다. malloc()이라는 함수가 메모리를 할당하는 함수이며 호출 성공 시 메모리 주소를 반환하고 실패하면 NULL을 반환합니다. 또한 이를 해제하기 위한 free()라는 함수도 존재하며 이 둘의 사용 과정에서 많은 실수가 발생하게 됩니다.

 

malloc()

그럼 먼저 malloc()에 대해 알아보겠습니다. malloc()의 호출은 매우 간단합니다. Heap에 요구하는 메모리 크기를 주면 해당 메모리의 포인터를 반환합니다. 오류가 나면 NULL을 반환하게 됩니다.

# include <stdlib.h>

void *malloc(size_t size);

위처럼 malloc을 사용하면 되는데, malloc의 매개변수는 요청할 바이트 수를 나타내는 값입니다. 요청할 바이트 수를 나타낼 때 숫자로 나타내는 것보다는 매크로나 고정값들을 사용하는 것이 좋습니다.

double *d = (double *) malloc(sizeof(double));

위와 같이 고정값들을 사용해주는 것이 좋습니다. 위의 코드에서는 sizeof(double) 값으로 이를 처리했는데요, 이렇게 sizeof()를 사용할 때 발생할 수 있는 문제는 무엇일까요?

int *x = malloc(10 * sizeof(int));
printf("%d\n", sizeof(x));

위와 같이 메모리를 할당하면 32비트 컴퓨터, 64비트 컴퓨터에서 각각 다르게 작동합니다. 이러한 문제는 아래처럼 해결할 수 있습니다.

int x[10];
printf("%d\n", sizeof(x));

하지만 이런 문제보다는 문자열을 사용할 때 더 큰 문제가 발생할 수 있습니다. C언어에서 문자열은 마지막에 항상 NULL을 가지게 되는데 이 때문에 sizeof가 원하는 값을 안 줄 수도 있습니다.

 

free()

그럼 이번에는 할당된 메모리를 해제하는 free()에 대해 알아보겠습니다. 사실 free()는 malloc() 보다 더 쉽습니다. 그냥 메모리를 할당한 포인터를 매개변수로 주기만 하면 됩니다.

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

 

Common Errors

이번 섹션에서는 malloc(), free()를 사용할 때 발생할 수 있는 실수들에 대해 알아보겠습니다. 또한 이러한 실수를 JAVA와 같은 언어에서는 Garbage collector라는 것이 도입되어 참조하지 않는 메모리를 자동으로 해제합니다. 제가 주로 공부하는 Swift에서는 ARC라는 개념으로 이러한 실수를 방지해줍니다.

 

그럼 이제 이러한 것이 없는 C언어에서는 어떤 실수가 발생할 수 있는지 알아보겠습니다.

Forgetting To Allocate Memory

많은 루틴은 호출 전 메모리가 할당되었다고 예상합니다. 그럼 호출 전 메모리가 할당되지 않았을 때 어떤 문제가 발생할까요? strcpy(dst, src)라는 문자열을 복사하는 함수로 이를 알아보겠습니다.

char *src = "hello";
char *dst; // 메모리 할당 하지 않았음
strcpy(dst, src); // segmentation fault 발생

위의 코드는 문자열을 복사할 변수에 메모리를 할당하지 않았기 때문에 segmentation fault를 발생시키고 프로그램이 죽습니다. 그럼 이를 보완한 코드를 한 번 보겠습니다.

char *src = "hello";
char *dst = (char *) malloc(strlen(src) + 1);
strcpy(dst, src); // 잘 동작됩니다!

여기서 strlen(src) + 1 만큼의 메모리를 할당해주는 이유는 C언어에는 모든 문자열의 마지막에 NULL 문자가 존재하기 때문에 이를 위한 메모리도 할당해줘야 하기 때문입니다. 그렇다면 이를 처리해주지 않으면 어떤 문제가 발생할까요?

Not Allocating Enough Memory

이번에는 충분한 메모리를 할당해주지 않았을 때 발생할 수 있는 문제에 대해 알아보겠습니다. 아까 본 코드에서 문자열의 마지막에 존재하는 NULL 문자를 위한 메모리를 할당해주지 않아 보겠습니다.

char *src = "hello";
char *dst = (char *) malloc(strlen(src)); // NULL 문자의 메모리 할당 하지 않음
strcpy(dst, src); // 동작은 잘 됩니다

위와 같이 코드를 작성하면 실행은 될 수 있습니다. 하지만 문자열의 마지막에 존재하는 NULL 문자를 위한 메모리를 할당해주지 않았기 때문에 buffer overflow와 같은 보안상 문제가 발생할 수 있습니다. 즉 이렇게 코드를 만들면 안 됩니다!

Forgetting to Initialize Allocated Memory

이 문제는 변수에 malloc()으로 메모리를 할당했지만 아무것도 입력하지 않은 경우입니다. 이렇게 되면 Heap의 메모리를 할당하지만 이렇게 할당받은 Heap의 메모리에 뭐가 있을진 아무도 모릅니다. 즉 이상한 데이터가 있을 수 있기 때문에 이렇게 메모리 할당만 하는 것은 좋지 않습니다.

Forgetting To Free Memory

그렇다면 메모리를 할당만 하고 해제하지 않으면 어떻게 될까요? 메모리 할당을 해제하지 않은 부분이 낭비되게 됩니다. 메모리에 공간이 부족하면 결국에는 프로그램을 다시 시작해야 하는 문제가 발생할 수 있기 때문에 다 사용한 메모리는 반드시 해제해야 합니다.

 

물론 프로그램의 수명이 아주 짧아 free()로 메모리를 해제해주는 것이 아닌 프로세스가 종료되면서 할당된 모든 페이지의 메모리를 해제하는 방법도 있습니다. 하지만 이는 좋은 방법이 아니기 때문에 항상 메모리를 다 사용하면 해제하는 습관을 갖는 것이 좋습니다!

Freeing Memory Before You Are Done With It

그렇다면 메모리를 사용 중인데 해제해버리면 어떻게 될까요? 이러한 실수를 dangling pointer(댕글링 포인터)라고 합니다. 이렇게 되면 포인터는 해제된 메모리를 가리키게 되고 이는 좋지 않습니다. 또한 후에 malloc()으로 새로 메모리를 할당할 때 포인터가 가리키고 있는 해제된 메모리 부분을 할당하게 되면 잘못된 데이터를 사용하게 될 수도 있습니다.

Freeing Memory Repeatedly

그렇다면 free()를 두 번 사용하여 메모리를 해제하면 어떻게 될까요? 이는 double free라고 불리는 실수로 누가 봐도 이상한 행동이기 때문에 충돌이 발생합니다.

Calling free() Incorrectly

이제 마지막 실수에 대해 알아보겠습니다. 이는 free()를 잘못 호출하는 것입니다. free()의 사용법을 기억하시나요? 해제하고 싶은 포인터를 매개변수로 입력받았었는데 실수로 해제하려는 것이 아닌 다른 것을 입력하게 된다면 문제가 발생하게 됩니다.

Common Errors Summary

이번 섹터에서 알아본 것은 C언어에서 메모리를 사용할 때 발생할 수 있는 실수와 오류에 대해 알아봤습니다. 실제 현업에서는 프로그램을 만든 뒤 메모리 사용에 실수는 없었는지를 확인하기 위해 purify, valgrind와 같은 것을 사용한다고 합니다.

Underlying OS Support

지금까지 알아본 malloc(), free()를 설명할 때 system call를 언급하지 않았습니다. 이는 malloc(), free()가 라이브러리 호출이기 때문입니다. 즉 malloc(), free()는 가상 주소 공간의 공간을 관리하며 메모리가 더 필요할 때 system call로 OS에게 이를 요청하게 됩니다.

 

이때 사용하는 system call 중 하나는 sys_brk()입니다. 이는 프로그램이 끝나는 위치 즉 힙의 끝을 변경하는 데 사용합니다. 이 말은 힙의 크기를 늘리거나 줄일 수 있다는 뜻이 됩니다. 이를 개발자가 직접 호출하면 안 되고 아까 언급했듯 라이브러리 호출에서 사용되게 해야 합니다.

 

sys_brk() 말고도 사용되는 system call은 sys_mmap()입니다. 이는 OS에게 메모리를 얻는 호출로 메모리 영역을 새로 만들 수 있습니다. 이렇게 만든 영역을 Heap처럼 사용하는 것인데 나중에 가상 메모리 글에서 자세히 알아보겠지만 간단하게만 알아보겠습니다.

위와 같이 호출들이 이뤄지는데 malloc()으로 100바이트의 메모리를 요청했다고 가정하겠습니다. 이를 system call로 할당받을 때 정확히 100 바이트를 가져오는 것이 아닌 한 번에 많이 가져오게 됩니다. 그런 뒤 이렇게 가져온 부분을 Heap 처럼 사용하는 것이죠. 일종의 cache 개념이라고 볼 수 있습니다. 이렇게 하면 system call을 호출하는 시간을 아낄 수 있습니다!

Other Calls

메모리 할당 라이브러리에는 malloc(), free() 말고도 다른 호출들도 있습니다. calloc()은 메모리를 할당하고 반환하기 전에 메모리를 0으로 초기화합니다. calloc()을 사용하면 아까 위에서 본 실수들 중 "Forgetting to Initialize Allocated Memory"를 방지할 수 있습니다! realloc()도 있는데 이는 현재 할당된 부분보다 좀 더 큰 부분을 할당하고 싶을 때 사용할 수 있습니다.

Summary

이번 글에서는 메모리 할당을 다루는 API에 대해 알아보았고 이를 사용할 때 할 수 있는 실수에 대해 알아보았습니다. 

감사합니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함