[OSTEP] Virtualization) 14. Memory API

0

OSTEP 운영체제

목록 보기
9/19
post-thumbnail
post-custom-banner

[OSTEP] 14. Memory API

이 포스팅은 <<Operating Systems: Three Easy Pieces>>, Remzi H. Arpaci-Dusseau & Andrea C. Arpaci-Dusseau을 읽고 개인 학습용으로 정리한 글입니다.

CH 14. 막간: 메모리 관리 API

  • UNIX의 메모리 관리 인터페이스
  • 어떻게 메모리를 할당하고 관리해야 하는가?

1. 메모리 공간의 종류

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

(1) 스택(stack) 메모리

  • 할당과 반환 컴파일러에 의해 암묵적으로 이루어짐
    (= 자동(automatic) 메모리)

  • ex. func()라는 함수 안에서 x라고 불리는 정수를 위한 공간이 필요한 경우

void func(){
	int x; //스택에 int 형을 선언
    ...
}
  • 컴파일러가 func()가 호출될 때 스택에 공간 확보 & 함수에서 리턴하면 메모리 반환
    -> 함수 리턴 이후에도 유지되어야 하는 정보는 스택에 저장하지 않는 것이 좋다

(2) 힙(heap) 메모리

  • 오랫동안 값이 유지되어야 하는 변수를 위함

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

  • ex. func()라는 함수 안에서 x라고 불리는 정수에 대한 포인터를 힙에 할당하는 경우

void func(){
	int *x = (int *) malloc(sizeof(int));
    ...
 }
  • 한 행에 스택과 힙 할당이 모두 발생한다
    -> 컴파일러가 포인터 변수의 선언(int * x)를 만나면 정수 표인터를 위한 공간 할당 (= 스택 할당)
    -> 프로그램이 malloc()을 호출하여 정수 포인터를 위한 공간을 힙으로부터 요구한다 (= 힙 할당)
    -> malloc()이 반환한 주소는 스택에 저장되어 프로그램에 의해 사용된다

2. malloc() 함수

  • malloc()의 호출: 힘에 요청할 공간의 크기를 넘겨주면
    -> 성공한 경우: 새로 할당된 공간에 대한 포인터 사용자에게 반환
    -> 실패한 경우: NULL 반환

malloc을 사용하기 위한 헤더 파일 stdlib.h

  • 헤더 파일을 꼭 소스 코드에 포함시켜야 하는 것은 아님
    -> 모든 C 프로그램에 디폴트로 링크되는 C 라이브러리는 malloc() 함수를 가짐

  • 헤더 파일을 추가하면 malloc()을 올바르게 호출했는지 컴파일러가 검사 가능
    -> 전달된 인자의 개수가 맞는지, 올바른 데이터 타임의 인자를 전달했는지 검사

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

sizeof() 연산자 사용

  • 정확한 크기의 공간을 요청하기 위해 sizeof()연산자 사용

  • ex. double precision의 부동 소수점 값을 위한 공간

   double *d = (double *) malloc(sizeof(double));
  • 기대한 결과를 얻지 못하는 경우:
    int *x = malloc(10*sizeof(int));
    printf("%d\n", sizeof(x));

-> 첫 번째 행에서 정수형 원소 10개를 가지는 배열을 위한 공간 선언
-> 두 번째 행에서 sizeof(x)를 사용하면, 동적으로 할당받은 메모리의 크기가 아닌 정수 포인터 x의 크기가 얼마인지 출력

  • ⚡기대한 결과를 얻는 경우:
    int x[10];
    printf("%d\n", sizeof(x));

-> 이 경우에는 변수 x에 40바이트가 할당되었다는 것을 컴파일러가 알 수 있는 정적인 정보가 충분하다

문자열을 위한 공간을 선언하는 경우

    malloc(strlen(s)+1)

-> strlen() 함수를 사용하여 문자열의 길이 얻어냄
-> 문자열의 끝을 나타내는 문자(\n)을 위한 공간 확보를 위해 1바이트 더함

  • malloc()은 void 타입에 대한 포인터 반환
    ->해당 주소 공간에 어떤 타입의 자료를 저장할 지는 프로그래머가 결정하게 하는 C의 방식

  • 프로그래머는 타입 반환(type casting) 을 이용하여 공간 활용 결정
    -> 프로그램이 제대로 작동하는 데 있어서 캐스트가 반드시 필요한 것은 아님

3. free() 함수

  • 더 이상 사용되지 않는 힙 메모리를 해제하기 위해 프로그래머는 free()를 호출
int *x = malloc(10*sizeof(int));
...
free(x);
  • free()는 malloc()에 의해 반환된 포인터를 받는다
    -> 할당된 영역의 크기는 전달되지 않는다
    -> 할당된 메모리의 크기는 메모리 할당 라이브러리가 알고 있어야 함

4. 흔한 오류

(1) 메모리 할당 잊어버리기

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

  • ex. strcpy(dst, src) 루틴: 소스 포인터에서 목적 포인터로 문자열 복사

char *src = "hello";
char *dst; //할당되지 않은 포인터
strcpy(dst, src); //segfault 발생

-> 이 코드를 실행하면 세그멘테이션 폴트(segmentation fault) 발생

char *src = "hello";
char *dst = (char *) malloc(strlen(src) + 1); 
strcpy(dst, src); //제대로 동작

-> ⚡대안으로, strdup()을 사용할 수 있다

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

  • = 버퍼 오버플로우(buffer over-flow)
char *src = "hello";
char *dst = (char *) malloc(strlen(scr)); //너무 작다
strcpy(dst, src); 

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

  • malloc()을 제대로 호출했지만, 새로 할당받은 데이터 타입에 특정 값을 넣는 것을 잊은 경우
    -> 초기화되지 않은 읽기(uninitialized read)

(4) 메모리 해제하지 않기

  • 메모리 청크의 사용이 끝나면 반드시 해제해야 한다

  • 메모리 누수 (memory leak)
    = 메모리 해제를 잊었을 때 발생

  • 장시간 실행되는 응용 프로그램/시스템 프로그램(ex. 운영체제 그 자체)에서 큰 문제임
    -> 메모리 누수 -> 메모리 부족 -> 시스템 재시작할 수 밖에 없음

  • 쓰레기 수집 기능이 있는 언어도 메모리 누수 발생
    -> 메모리 청크에 대한 참조가 존재하면 쓰레기 수집기가 청크를 해제하지 않기 때문

  • 프로그램 실행 시간이 짧아, 실행 후 곧 종료하는 경우
    -> 프로세스가 죽으면, 운영체제는 할당된 페이지를 모두 정리
    -> 메모리 누수 일어나지 않는다

  • 하지만, 이와 같은 전략은 매우 조심해서 사용해야 한다

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

  • 메모리 사용이 끝나기 전에 메모리 해제 (= dangling pointer)

  • 차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다
    -> 예를 들어, free()를 호출하고, 그 후 다른 용도로 malloc()을 호출하면 잘못 해제된 메모리를 재사용한다

  • free는 다시 malloc으로 할당 받을 수 있게 해제하는 역할이라고 생각할 수 있다

  • (1) x라는 포인터에 malloc으로 10바이트를 할당
    (2) y라는 포인터에 malloc으로 20바이트를 할당
    -> 두 메모리는 겹치지 않는다

  • (1) x라는 포인터에 malloc으로 10바이트를 할당
    (2) free(x)
    (3) y라는 포인터에 malloc으로 20바이트 할당
    -> free(x)로 해제한 메모리의 일부 또는 전부를 할당 받을 수 있다

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

  • 메모리를 한 번 이상 해제
    -> 이중 해제(double free) 발생

  • 결과는 예측하기 어렵다
    -> 가장 흔히 일어나는 결과: 크래시

(7) free() 잘못 호출하기

  • free()는 malloc() 받은 포인터만 전달될 것으로 예상
    -> 그 이외의 값을 전달하면 문제 발생
    -> 유효하지 않은 해제(invalid trees) 발생

(8) 요약

  • ⚡코드에서 메모리 관련 오류를 찾아내는 도구: puritfy, valgrind

5. 운영체제의 지원

  • malloc()과 free()는 시스템 콜이 아니라 라이브러리 함수이다
    -> malloc 라이브러리가 프로세스 가상 주소 공간 안의 공간 관리
    -> 라이브러리 자체는 시스템에게 더 많은 메모리를 요구하고 반환하는 시스템 콜을 기반으로 구축됨

brk 시스템 콜

  • 라이브러리 구축에 사용되는 시스템 콜 중 하나

  • 프로그램의 break 위치를 변경하는 데 사용
    -> break는 힙의 마지막 위치를 나타낸다

  • brk는 새로운 break 주소를 나타내는 한 개의 인자를 받는다
    -> 새로운 break > 현재 break: 힙의 크기 증가
    -> 새로운 break < 현재 break: 힙의 크기 감소

  • sbrk 시스템 콜 : brk와 비슷한 용도로 사용됨
    (sbrk는 증가량만을 받아들인다)

  • brk와 sbrk는 메모리 할당 라이브러리에 의해 사용됨
    -> 직접 호출해서는 안 된다

⚡mmap() 함수

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

  • 올바른 인자를 전달하면 mmap()는 프로그램에 anonymous의 메모리 공간을 만든다
    -> anonymous 영역: 특정 파일과 연결되어 있지 않고 스왑 공간(swap space) 에 연결된 영역

6. 기타 함수들

  • calloc() 함수: 메모리 할당 영역을 0으로 채워서 반환
    -> 초기화 하는 것을 잊어버리는 오류 방지

  • realloc() 함수: 이미 할당된 공간에 대해 추가의 공간이 필요할 때 유용 (ex. 배열)
    -> 더 큰 새로운 영역을 확보 -> 옛 영역의 내용을 복사 -> 새 영역에 대한 포인터 반환

profile
Be able to be vulnerable, in search of truth
post-custom-banner

0개의 댓글