TIL

[TIL] 동적 메모리 할당 Dynamic Allocation

아람2 2024. 10. 8. 11:24
반응형

동적 메모리 할당 Dynamic Allocation 

컴퓨터 프로그래밍에서 실행 시간 (런타임) 동안 사용할 메모리 공간을 할당하는 것
사용이 끝나면 운영 체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재할당 받을 수 있다 
동적 할당은 프로세스의 힙 영역에서 할당한다 

( 힙 영역에 저장되는 게 있고 아닌 게 있음 추가 공부 필요 ) 

동적 메모리 할당이 필요한 이유

메모리 공간은 한정되어 있기 때문에 그때그때 필요한 만큼만 메모리 공간을 확보하고,

다 사용했다면 Free 시켜주어 메모리 공간을 해제함으로서 한정된 메모리 공간을 효율적으로 사용할 수 있다 

 

동적 메모리 할당의 장단점

장점 

* 상황에 따라 원하는 크기만큼의 메모리가 할당되므로 경제적이다 (malloc or calloc)

* 이미 할당된 메모리라도 언제든 크기를 조정할 수 있다 (realloc)

단점

* C언어의 경우 Garbage Collector 가 없기 때문에, 개발자가 명시적으로 메모리를 해제해 주어야 한다

* 만약 이를 하지 않았을 경우 메모리 누수가 나타나고 이는 디버깅하기 매우 까다롭다 

 

 동적 메모리 할당 사용 함수 

함수  기능
void * malloc (size_t size); size 바이트의 메모리를 힙에서 할당하여 반환한다 
void * calloc (size_t num, size_t size); (num * size) 바이트의 메모리를 힙에서 할당하고 포인터 값을 반환한다 
void * realloc (void *ptr, size_t size); ptr 이 가리키는 메모리를 size 바이트만큼 힙에서 재할당하여 반환한다
void free (void * ptr); ptr 이 가리키는 메모리를 해제한다 

 

동적 메모리 할당 사용 예시 

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

int main() {
    // 동적으로 5개의 정수 크기의 메모리를 할당
    int* arr = (int*)malloc(5 * sizeof(int));
    
    // 메모리 할당에 실패한 경우, NULL 반환
    if (arr == NULL) {
        printf("메모리 할당 실패\n");
        return -1; // 비정상 종료를 나타내기 위해 -1 반환 
    }

    // 배열 요소에 값 할당
    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;  // 값 설정
    }

    // 배열 요소 출력
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);  // 값 출력
    }
    
    // 메모리 해제
    free(arr);

    return 0;
}

 

+ 운영체제 책 14장 내용

14.1 메모리 공간의 종류

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

1) 스택 Stack 

컴파일러에 의해 할당과 반환이 암묵적으로 이루어지기 때문에 자동 Automatic 메모리라고 불린다 

func() 라는 함수 안에서 x 라 불리는 정수를 선언할 때 아래와 같이 선언한다 

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

 

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

함수가 리턴할 때 컴파일러는 프로그래머 대신에 메모리를 반환한다 

-> 스택 메모리에 저장되는 변수는 함수가 리턴할 때 메모리가 반환된다 

2) 힙 Heap

모든 할당과 반환이 프로그래머에 의해 명시적으로 처리된다 

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

 

컴파일러가 포인터 변수의 선언 (int * x) 을 만나면 정수 포인터를 위한 공간을 할당해야 한다는 것을 안다 

프로그램이 malloc() 을 호출하여 정수를 위한 공간을 힙으로부터 요구하고, malloc() 은 그 정수의 주소를 반환한다 

(실패한 경우에는 NULL 반환) 이 반환된 주소는 스택에 저장되어 프로그램에 의해 사용된다 

14.2 malloc() 함수 

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

 

malloc() 의 인자는 size_t 타입의 변수이고 이 변수는 필요 공간의 크기를 바이트 단위로 표시한 것이다 

C 언어에서 sizeof() 는 통상 컴파일 시간 연산자이고, 인자의 실제 크기가 컴파일 시간에 결정된다 

sizeof() 는 숫자로 대체되어 malloc() 에 전달되기 때문에 sizeof() 는 연산자로 간주된다 (함수 호출이 아니다)

 

sizeof() 로 문자열을 다룰 때 조심해야 한다, 문자열을 위한 공간을 선언할 때는 malloc(strlen(s) + 1) 문장을 사용한다 

strlen(s) 함수는 널 종료 문자인 '\0' 을 제외하고 문자열의 길이를 계산하기 때문에 malloc(strlen(s)) 을 쓰면 문제를 일으킬 수 있다 

14.3 free() 함수 

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

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

 

한 개의 인자, malloc() 에 의해 반환된 포인터를 받는다 

할당된 영역의 크기는 전달되지 않지만, 메모리 할당 라이브러리는 할당된 메모리의 크기를 알고 있어야 한다 

14.4 흔한 오류

많은 새로운 언어들이 자동 메모리 관리 Automatic Memory Management 를 지원한다 

그러한 언어들에서는 공간을 해제하기 위해서 아무것도 호출하지 않고

쓰레기 수집기 Garbage Colletor 가 실행되어 참조되지 않는 메모리를 찾아 알아서 해제한다 

메모리 할당 잊어버리기 

많은 루틴은 자신이 호출되기 전에 필요한 메모리가 미리 할당되었다고 가정한다 

예를 들어 strcpy(dst, src) 루틴은 소스 포인터에서 목적 포인터로 문자열을 복사한다 

// 메모리 할당 잊어버리기의 예시 
char *src = "Hello";
char *dst; 	// 할당이 안 되어 있다 
strcpy(dst, src); // segfault 그리고 죽는다

 

이 코드를 실행하면 세그멘테이션 폴트 Segmentaion Fault 가 발생할 가능성이 높고,

이 폴트는 "네가 메모리 관련 무언가를 잘못했어, 이 바보 같은 프로그래머야, 그래서 나 화났거든" 이라는 메시지이다

// 메모리 할당을 잘 한 예시
char *src = "Hello";
char *dst = (char *) malloc(strlen(src)+1);
strcpy(dst, src); // 제대로 동작

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

버퍼 오버플로우 Buffer Overflow 라고 불린다 

char *src = "Hello";
char *dst = (char *) malloc(strlen(src)); // 너무 작다
strcpy(dst, scr); // 동작은 제대로 한다

 

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

이 공간이 더 이상 사용되지 않는 변수 영역이라면 덮어쓰더라도 피해가 발생하지 않을 수 있지만 

다른 때에는 이러한 오버플로우가 매우 유해할 수 있고, 사실 많은 시스템에서 보안 취약점의 원인이다 [Wer06] 

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

malloc()을 제대로 호출했지만 새로 할당받은 데이터 타입에 특정 값을 넣는 것을 종종 잊는다 

초기화되지 않는 읽기 Uninitialized Read 는 알 수 없는 값을 읽는 일이다 

메모리 해제하지 않기

메모리 누수 Memory Leak 은 메모리 해제를 잊었을 때 발생한다 

메모리 청크의 사용이 끝나면 반드시 해제해야 한다, 쓰레기 수집 기능이 있는 언어도

메모리 청크에 대한 참조가 존재하면 그 청크를 해제하지 않아 메모리 누수가 생길 것이다 

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

메모리 사용이 끝나기 전에 메모리를 해제하는 실수는 Dangling Pointer 라고 불리며 심각한 실수이다 

차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다 

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

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

프로그램은 가끔씩 메모리를 한 번 이상 해제하며 이중 해제 Double Free 라 불린다 

그런 상황에서 메모리 할당 라이브러리는 크래시를 비롯한 모든 종류의 이상한 일을 하게 된다 

free() 잘못 호출하기 

malloc() 받은 포인터 값 외에 값으로 free() 를 전달하면 문제가 발생한다 

유효하지 않은 해제 Invalid Frees 는 매우 위험하고 당연히 피해야한다 

 

14.5 운영체제의 지원 

malloc 라이브러리는 프로세스 가상 메모리 공간안의 공간을 효율적으로 관리하는 역할을 하지만

라이브러리 자체는 시스템에게 더 많은 메모리를 요구하고 반환하는 시스템 콜을 기반으로 구축된다 

 

그런 시스템 콜 중 하나가 brk 라고 불리는, 프로그램의 break 위치를 변경하는 시스템 콜이다 

break 는 힙의 마지막 위치를 나타내고, brk 는 새로운 break 주소를 나타내는 한 개의 인자를 받는다 

 

mmap() 함수를 사용하여 운영체제로부터 메모리를 얻을 수 있다

올바른 인자를 전달하면 mmap() 은 프로그램에 anonymous 의 메모리 영역을 만든다 

(anonymous 영역은 특정 파일과 연결되어 있지 않고 스왑 공간 swap space 에 연결된 영역이고,

이 메모리는 힙처럼 취급되고 관리된다 

brk/ sbrk 의 정의 
힙은 Uninitialized된 데이터 영역 (bss) 직후에 시작해서 위쪽으로 메모리 주소가 커지는 영역이고 
brk, sbrk는 현재 프로세스의 program break 힙의 꼭대기를 가리키는 변수)의 위치를 변경한다 
brk와 sbrk 함수를 사용하여 힙의 크기를 조정할 수 있다 
* Program Break를 증가시켜 메모리 할당 효과 
* Program Break를 감소시켜 메모리 할당 해제 효과 
출처 https://velog.io/@stok97/SW%EC%A0%95%EA%B8%80Malloc-Lab-brk-sbrk

14.6 기타 함수들

calloc() 은 메모리 할당 영역을 0 으로 채워서 반환한다 

realloc() 은 이미 할당된 공간에 대해 추가의 공간이 필요할 때 더 큰 새로운 영역을 확보하고

옛 영역의 내용을 복사한 후에 새 영역에 대한 포인터를 반환한다 

 

반응형