책너두 (컴퓨터 밑바닥의 비밀) 19일차 대표적인 메모리 관련 버그

  • 지역 변수의 포인터 반환하기
int* func()
{
    int a = 2;
    return &a;
}

void main()
{
    int* p = func();
    *p = 20;
}
  • 지역 변수 a가 func 함수의 스택 프레임에 위치함.

    • func 함수의 실행이 끝나면 해당 스택 프레임도 없어짐.
    • 즉, main 함수의 func 함수를 호출한 후 얻은 포인터는 이미 없는 변수를 가리킴.
  • 만약 위 상황에서, 추가적으로 foo 와 같은 함수가 호출되면 포인터 p의 내용을 foo 함수의 스택 프레임이 덮어 쓰게됨.

    • 혹은, 포인터 p가 가리키는 값을 수정하면 실제로 foo 함수의 스택 프레임이 파괴됨.
  • 포인터 연산의 잘못된 이해

    • 포인터 연산에 1을 더하는 것은 1바이트만 이동하는 것이 아님.
    • 단위 한 개만큼 이동함.
      • ex) int 형 포인터라면 포인터에 1을 더하는 것은 4바이트 만큼 이동하는 것임.
  • 문제 있는 포인터 역참조하기

int a;
scanf("%d", a);
  • a 주소값이 아닌 a 그자체를 scan 하고 있는 상황임.
  1. a 값이 코드 영역이나 기타 읽기 전용 영역을 가리키는 포인터 값으로 해석되면 운영 체제는 이 프로세스를 즉시 강제 종료함.
  2. a 값이 스택 영역을 가리키는 포인터 값으로 해석되면 다른 함수의 스택 프레임이 파괴되었기 때문에 버그가 발생하고 원인을 찾기도 힘듦.
  3. a 값이 힙, 데이터 영역을 가리키는 포인터 값으로 해석된다면 프로그램이 동적으로 할당한 메모리가 파괴되었기 때문에 버그가 발생하고 원인을 찾기도 힘듦.
  • 초기화되지 않은 메모리 읽기
void add()
{
    int* a = (int*)malloc(sizeof(int));
    *a += 10;
}
  • malloc 호출할 때 실제로 두 가지 가능성이 존재함.
  1. malloc 이 자체적으로 충분한 메모리를 유지하고 있다면 malloc 은 여유 메모리 조각에서 반환할 주소를 찾음.

    1. 이 메모리는 이미 사용되었을 수 있으며, 이전에 사용한 정보가 남아 있을 수 있으므로 값이 0이 아닐 수 있음.
  2. malloc 이 메모리가 충분치 않다면 brk 같은 시스템 호출로 운영 체제에 메모리를 요청함.

    1. 페이지 누락 인터럽트가 발생하면서 운영체제가 실제 물리 메모리 할당할 때 0으로 초기화 됨.

      → 이유는 운영 체제에서 반환한 메모리는 이전에 다른 프로세스에서 사용했을 수 있으며 여기에 민감한 정보가 포함되어있을 수 있기 때문에 다른 프로세스에서 정보를 읽지 못하도록 운영 체제가 메모리 반환하기 전에 0으로 초기화함.

  • 결론은, malloc 이 반환한 메모리가 당연히 0으로 초기화되어 있다고 가정하면 안됨.

  • 이미 해제된 메모리 참조하기

void add()
{
    int* a = (int*)malloc(sizeof(int));
    ...
    free(a);

    int b = *a;
}
  • 포인터 a가 가리키는 메모리 조각이 해제된 후 아직 malloc으로 다시 할당되지 않았다면 a가 가리키는 값은 이전과 동일함.

  • 포인터 a가 가리키는 메모리 조각이 이미 malloc으로 할당 되었다면 a가 가리키는 메모리는 이미 덮어쓰기가 되었을 수 있음.

    • 이러한 종류의 문제는 프로그램이 장시간 실행되고 나서야 발견될 수 있음.
    • 즉, 문제를 찾기 매우 어려움..
  • 배열 첨자는 0부터 시작한다.

void init(int n)
{
    int* arr = (int*)malloc(n * sizeof(int));

    for (int i = 0; i <= n; i++)
    {
        arr[i] = i;
    }
}
  • 배열 첨자는 0부터 시작하는데, 탐색을 n+1 개만큼 하고 있고, 배열 값을 덮어쓰고 있음.

  • 만약, malloc 이 반환한 메모리 자체가 n * sizeof(int) 보다 크다면 그 메모리를 덮어써도 문제는 안됨.

    • 근데, 덮어쓴 메모리에 malloc 이 사용하는 메모리 할당 상태 정보가 있다면 malloc 동작이 파괴됨..
  • 스택 넘침

void buffer_overflow()
{
    char buf[32];
    gets(buf);

    return;
}
  • 이 코드는 사용자 입력이 32바이트를 초과하지 않는다고 가정함.

    • 하지만 일단 초과하는 순간 스택 프레임 내에 인접한 데이터를 파괴함.
  • 함수 스택 프레임이 파괴되었을 때, 가장 이상적인 결과는 프로그램이 즉시 충돌하여 강제 종료되는 것임.

    • 그렇지 않다면 프로그램이 장시간 실행되다 갑자기 오류가 발생하거나 잘못된 결과가 계속 제공될 수 있음.
  • 스택 프레임에는 함수 반환 주소처럼 중요한 정보가 들어가있음.

    • 따라서, 스택 버퍼 넘침은 문제를 일으킬 가능성이 힙 영역의 데이터 넘침 보다도 더 높음.
  • 메모리 누수

void memory_leak()
{
    int* p = (int*)malloc(sizeof(int));
    return;
}
  • 메모리 요청 후 바로 반환하면, 해당 메모리는 프로세스 종료 전에 다시 해제할 방법이 없음.
    • 메모리 누수가 일어남.
  • 메모리를 계속 요청하기만 하고 해제하지 않으면 프로세스의 힙 영역이 점점 늘어나 결국 운영 체제가 강제로 프로세스를 종료하는 상황이 발생함.
    • out of memory killer

댓글

Designed by JB FACTORY