본문 바로가기

Programming/C/C++

C/Pointer/C포인터의 이해와 활용 - 2

반응형

※ 본 자료는 위 책을 읽고 개인적으로 정리한 포스트임을 알려드립니다.


# 동적 메모리 관리
- 포인터의 강력한 기능의 대부분은 동적으로 할당된 메모리를 추적할 수 있는 포인터의 능력에서 기인한다.
  • 동적 메모리 관리는 복잡한 데이터 구조를 조작하는 일을 포함한 많은 일의 핵심이다.
- C 프로그램은 런타임 시스템(runtime system) 안에서 실행된다.
  • 런타임 시스템 : 운영체제에서 제공되는 환경
    • 많은 프로그램 기능들과 함께 스택(stack), 힙(heap)을 지원

- 메모리 관리 : 프로그램의 핵심
  • 자동 관리 : 자동 변수의 메모리 할당
  • 정적 변수와 전역 변수의 경우는 프로그램 시작 시 0으로 초기화되는 애플리케이션의 다른 데이터 세그먼트에 위치함

- 메모리를 할당하고 해제하는 능력
  • 메모리를 더 효율적이며 유연하게 관리함
  • 동적 메모리 할당 능력은 연결 리스트와 큐 강튼 가변 요소를 사용하는 데이터 구조를 구현할 때 도움

- C99 표준 :  가변 길이 배열(Variable Length Array)이 도입
  • 이 배열의 크기는 컴파일 될 때가 아니라 실행 될때 결정됨 

- 동적 메모리 관리 (dynamic memory management) 
  • 힙(Heap)에서 메모리를 할당하는 동적 메모리 관리를 지원한다.
  • 동적 메모리 할당은 할당, 해제 함수를 사용하여 수동으로 처리

# 동적 메모리 할당
- stdlib.h 헤더 파일에서 관련 함수를 찾을 수 있음
  • malloc류 함수로 메모리 할당
  • 애플리케이션에서 할당된 메모리 사용
  • free 함수를 이용해 할당된 메모리를 해제

- 실제 사용
1
2
3
4
5
int *pi = (int*malloc(sizeof(int));
*pi = 5;
printf("*pi: %d\n"*pi);
free(pi);
 
  • 메모리 할당에 성공하면 할당된 메모리의 포인터를 반환
  • 실패하면 널 포인터를 반환
  • sizeof 연산자를 사용하면 애플리케이션에 대한 이식성을 높일 수 있음

# 메모리 누수
- 상황
  • 메모리의 주소를 잃어버리는 경우
  • free 함수가 호출되어야 하는 상황에 호출되지 않는 경우
- 결론
  • 메모리의 양 감소
  • OOM 오류 발생(Out of Memory)
  • 프로그램 종료

- 숨겨진 메모리 누수
  • 메모리를 해제해야할 때 해제하지 못하는 경우 발생
  • 구조체를 먼저 해제 했을 때 구조체 안에 동적으로 할당했던 포인터를 해제 하지 않은 것

# 메모리 주소 손실
- pi 포인터를 해제 하지 않고 재사용할 때 이전에 할당된 메모리의 주소는 손실
1
2
3
int *pi = (int*malloc(sizeof(int));
*pi = 5;
pi = (int*malloc(sizeof(int));

- 포인터의 증가를 통해 포인터의 시작위치를 잃어버리는 것
1
2
3
4
5
6
char *name = (char*malloc (strlen("Susan")+1);
strcpy(name, "Susan");
while(*name != 0){
    printf("%c"*name);
    name++;
}

# 동적 메모리 할당 함수
- stdlib.h
  • malloc : 힙에서 메모리 할당
  • realloc : 기존 할당된 메모리의 크기 변경
  • calloc : 힙에서 메모리 할당 그리고 0으로 설정
  • free : 할당된 메모리를 힙으로 반환

- malloc 함수 사용
  • 할당된 메모리에 대한 void 포인터를 반환
  • 할당할 수 없을 때 malloc 함수는 NULL을 반환
  • void* malloc(size_t);
  • 0을 전달하면 함수 행동 구현에 따라 NULL 포인터 또는 0바이트가 할당된 영역에 대한 포인터를 반환할 수도 있음
  • 단계
    • 힙 영역에서 메모리가 할당된다.
    • 메모리는 수정되거나 초기화되지 않는다.
    • 첫 번째 시작 주소가 반환된다.
  • 캐스팅은 필요한가?
    • 명시적 캐스팅은 malloc 함수의 사용 의도를 명확히 함
    • 명시적 캐스팅을 필요로 하는 C++ 또는 초기 C 컴파일러와 호환 가능한 코드를 생성
- 메모리 할당 실패
  • 할당에 실패한 포인터는 일반적으로 가비지 값을 포함
  • 런타임 예외(runtime exception)가 발생

# 정적 포인터 및 전역포인터에 malloc 사용
1
2
static int *pi;
pi = malloc(sizeof(int));

# calloc 함수 사용하기
1
void *calloc(size_t numElements, size_t elementSize);
- calloc 함수는 메모리 할당과 동시에 초기화를 한다.
  • numElements와 elementSize 인자 값에 의해 결정되는 크기만큼의 메모리를 할당하며, 할다된 메모리의 첫 바이트를 가리키는 포인터를 반환
  • 두 값중 하나라도 0이면, 널 포인터가 반환된다.
  • calloc 함수는 메모리를 할당할 수 없을 때 널 포인터를 반환하고, 전역 변수 errno가 ENOMEM(out of memory)으로 설정된다.
  • 대체
    • calloc함수 대신에 malloc 함수와 memset 함수를 같이 사용하면 calloc 함수를 사용하는 것과 같은 결과를 얻을 수 있다.
    1
    2
    int *pi = malloc(5*sizeof(int));
    memset(pi, 05* sizeof(int));
  • calloc 함수의 실행이 malloc 함수보다 시간이 오래 걸린다.

# realloc 함수 사용
- 포인터에 할당된 메모리의 크기를 늘리거나 줄이는 일이 필요할 때 사용
- 가변 배열 구현에 유용
- void * realloc(void *ptr, size_t size);
  • realloc 함수는 호출 시 두 개의 인자를 취하며, 재할당된 메모리 영역의 포인터를 반환한다.
  • 첫번째 인자는 기존 할당된 메모리에 대한 포인터
  • 두번째 인자는 요청할 메모리의 크기
    • 재할당된 메모리의 크기는 기존 메모리의 크기와 다르며 반환 값은 재할당된 메모리의 포인터

# alloca 함수와 가변 길이 배열
- alloca 함수(마이크로소프트 운영체제의 malloca 함수와 동일)는 함수 내부에서 사용할 목적으로 스택 프레임 안의 메모리를 할당
  • 함수(스택 프레임을 소유한)가 반환하면 alloca에 의해 할당된 메모리는 자동으로 해제됨.

# free  함수로 메모리 반환하기
- 동적 메모리 할당에서 프로그래머는 할당된 메모리가 더는 필요치 않을 경우, 다른 용도에 사용될 수 있도록 free 함수를 사용하여 메모리를 해제할 수 있다.
  • free 함수의 프로토타입 : void free(void *ptr);

# 해제된 포인터에 NULL 할당
- 포인터는 메모리가 해제된 이후에도 문제를 유발할 수 있음
  • 명시적으로 NULL을 할당
  • 이미 해제된 포인터의 사용은 런타임 예외를 발생

1
2
3
4
int *pi = (int*malloc(sizeof(int));
...
free(pi);
pi = NULL;

  • 이 기법은 댕글링 포인터 같은 문제를 해결하기 위한 것

# 이중 해제
- 이중 해제(Double Free)란 메모리 해제를 두 번 시도하는 것을 말함
1
2
3
4
5
int *pi = (int*malloc(sizeof(int));
*pit = 5;
free(pi);
...
free(pi);

# 에일리어싱(aliasing)
- 두포인터가 같은 메모리 위치를 참조하는 것
1
2
3
4
5
6
p1 = (int*malloc(sizeof(int));
int *p2 = p1;
free(p1);
...
free(p2);
 
  • 여기서 힙 관리자는 메모리가 해제될 때 메모리가 이미 해제되었는지 판단하지 못하고, 두 번 해제 한다.
  • 두 번 해제하는지도 판단하지 못한다.

# 힙 메모리와 시스템 메모리
- free 함수가 호출되었다고해서 힙 관리자가 반드시 해제된 메모리를 운영체제로 반환하는 것이 아니고, 애플리케이션 내에서 해당 메모리를 다시 사용할 수 있게 할 뿐, 프로그램이 메모리를 할당한 후 다시 해제한다고 해도 일반적으로 운영체제 측면에서는 해제된 메모리가 애플리케이션의 메모리 사용량에 반영되지 않는다.

# 프로그램 종료시 메모리 해제
- 운영체제는 메모리를 포함한 애플리케이션 리소스의 관리 책임이 있음.
  • 사용된 메모리를 다른 애플리케이션이 재할당할 수 있도록함
  • 수동적으로 사용 종료된 메모리를 해제하는 습관은 좋다.
  • 진단 도구로 메모리 누수를 쉽게 찾을 수 있다.

- 메모리 반환은 프로그램의 책임
  • 보장
    • 메모리 해제의 중요성에 비해 구현하기가 꽤 까다롭다.
    • 구현에 다로 시간을 써야 하며, 복잡한 구조체의 경우 구현이 더 복잫바다.
    • 애플리케이션의 크기가 늘어난다.
    • 애플리케이션의 실행 시간이 길어진다.
    • 메모리 해제 코드에서 새로운 오류가 발생하기도 한다.

# 댕글링 포인터
- 포인터가 여전히 해제된 메모리 영역을 가리키고 있다면, 이러한 포인터를 댕글링 포인터(Dangling Pointers)라고 한다.
  • 너무 빠른 해제(premature free)라고 불림.
  • 발생하는 문제
    • 메모리 접근 시 예측 불가능한 동작
    • 메모리 접근 불가 시 세그멘테이션 오류(Segmentation fault)
    • 잠재적인 보안 위협
    • 결과
      • 메모리 해제 후, 해제된 메모리에 접근
      • 함수 호출에서 자동 변수를 가리키는 포인터의 반환
- 예제 1
1
2
3
4
5
int *pi = (int*malloc(sizeof(int));
*pi = 5;
printf("*pi: %d\n"*pi);
free(pi);
*pi = 10;

- 예제 2
1
2
3
4
5
6
7
8
9
10
int *p1 = (int*malloc(sizeof(int));
*p1 = 5;
...
int *p2;
p2 = p1;
...
free(p1);
...
*p2 = 10// 댕글링 포인터
 
- 댕글링 포인터 다루기
  • 포인터가 원인인 문제들의 디버깅은 때로 해결하기 어려울 때가 있음.
    • 해결
      • 메모리 해제 후 포인터를 NULL로 설정
        • 다수의 포인터 복사본이 존재할 경우 복사본 중에 단 하나의 포인터에만 영향을 미치기 때문에 문제가 남아 있을 수 있다.
      • free함수를 대체할 새로운 함수 작성
      • 몇몇 런타임 시스템이나 디버깅 시스템은 해제된 메모리를 특별한 값을 덮어씀 
        • ex) 0xDEADBEEF, 0xCC, 0xCD, 0xDD
      • 댕글링 포인터를 발견하기 위해 서드파티 도구를 이용
- 메모리 누수 탐지
  • 할당된 메모리를 덮어쓰는 문제와 메모리 누수를 해결하기 위한 기술 : 마이크로소프트가 제공
    • 힙의 무결성 검사
    • 메모리 누수 검사
    • 힙 메모리가 부족한 상황 재현
  • 기술 정보 : http://bit.ly/12SftWV Microsoft Developer Network
  • Mudflap 라이브러리 http://bit.ly/YilPI1

# 동적 메모리 할당 기술
- 힙 관리자 종류 
  • OpenBSD의 malloc
  • Hoard의 malloc
  • 구글의 TCMalloc
  • GNU C 라이브러리의 dlmalloc (http://dmalloc.com/)
    • 이 라이브러리는 디버깅을 위한 기능이나 메모리 누수 시에 이를 추적하는 기능을 제공
- 메모리 할당과 해제 기술의 구현 방식은 컴파일러마다 다를 수 있다.
  • 대부분의 힙 관리자 (malloc)은 메모리를 할당하기 위해 데이터 세그먼트를 사용하며, 이 방식은 힙 메모리 단편화(fragmentation)나 프로그램 스택과 충돌이 일어나기도 한다.
  • 관리해야할 이슈
    • 프로세스 또는 스레드 단위로 할당할지 결정
    • 힙 보안 취약점 이슈
- 가비지 컬렉션
  • 문제
    • 성능 이슈
    • 메모리 참조 범위 제한
    • 스레드 문제
    • 깔끔한 메모리 해제(memory gracefully)
  • 해결
    • 자동 메모리 해제 기술 : 가비지 컬렉션(garbage collection)
      • 쓰는 이유
        • 메모리 해제 시점에 대한 고민으로 부터 프로그래머를 자유롭게 한다.
        • 프로그래머는 애플리케이션 자체의 문제에만 집중할 수 있다.
  • 대안
    • 수동 메모리 관리의 대안
      • BDW 가비지 컬렉터(Boehm-Demers-Weiser garbage collector)

# 리소스 획득 즉시 초기화 (RAII, Resource Acquisition Is Initialization)
- C++언어에서 리소스 할당과 해제
  • 예외 상황에서 유용
  • 비표준 확장 기능임 : GNU C 컴파일러 지원
    • GNU 확장 : RAII_VARIABLE 매크로 사용
      • 변수 타입
      • 변수가 생성될 때 호출할 함수
      • 변수가 범위를 벗어날 때 호출할 함수
      • 선언 문
      • 1
        2
        3
        #define RAII_VARIABLE(vartype, varname, initval, dtor) \
            void _dtor _ ## varname (vartype * v) {dtor(*v); } \
            vartype varname __attribute__(cleanup(_dtor_ ## varname))) = (initval)
- 예외 처리기 사용

  • 예외 처리기(Exception Handler)를 사용하여 다른 방법으로 메모리 해제 작업 가능
    • C표준 아님
    • try 블록 으로 예외가 일어나는 문장을 감싸고 finally 블록에 반드시 free가 되도록 한다.


반응형