[C]메모리(스택, 힙, 레지스터)


이번 포스트는 메모리(memory)에 관한 내용입니다.


1️⃣ 메모리

  • C언메니지드 언어입니다. 언어가 메모리 관리(manage)를 해주지 않고 사용자가 직접 관리를 하기 때문에 속도, 효율성, 메모리 절약등의 장점이 있습니다.
  • 하지만 관리를 제대로 하지않으면 메모리 누수가 일어날 수 있습니다. 그렇기 때문에 훌륭한 언메니지드 프로그래머가 되기 위해서는 메모리에 대해 잘알아야하며 메모리 누수와 같은 실수를 줄이기위해 여러가지 원칙들을 습관화하는 것은 중요합니다.

< 메모리 >

memory_img1

< 코드(code)영역 >

실행할 코드와 매크로 상수가 기계어의 형태로 저장된 공간입니다. CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리합니다.

< 데이터(data)영역 >

코드에서 선언한 전역변수와 정적변수(static) 등이 저장된 공간입니다. 프로그램의 시작과 함께 할당되며, 프로그래미 종료되면 소멸합니다.

< 스택(stack)영역 >

지역변수, 매개변수, 리턴값, 돌아올 주소등이 저장된 공간입니다. 컴파일 타임에 크기가 결정됩니다. 함수 호출시 기록하고 종료되면 제거합니다. 영역을 초과하면 stack ocerflow가 발생합니다.

< 힙(heap)영역 >

사용자에 의해 메모리 공간이 동적으로 할당되고 해제됩니다. 그렇기 때문에 런타임중에 크기가 결정됩니다. 제대로 메모리를 해제시켜주지 않으면 memory leak이 발생합니다.


2️⃣ 스택(stack) 메모리

  • 프로그램마다 특별한 용도에 사용하라고 별도로 떼어놔 준 것
  • 정적 메모리의 개념으로 이미 공간이 따로 잡혀 있습니다.
  • 높은 주소부터 스택이 쌓이며 오프셋 개념으로 정확히 몇 바이트씩 사용해야 하는지 컴파일시 결정합니다.

< 스택 메모리의 장점 >

  • 할단/해제가 자동으로 관리되게 코드가 컴파일됩니다.
  • 오프셋 개념으로 메모리를 관리하다보니 속도가 빠릅니다.

< 스택 메모리의 단점(1) - 수명 >

  • 함수가 종료되면 그 안에 있던 데이터가 다 날아갑니다.
  • static키워드를 이용하여 전역변수로 데이터를 오래 보존하는 것이 가능합니다.
  • 이 처럼 스택 메모리의 수명모 아니면 도여서 중간이 없다.
  • 동적 메모리의 개념으로 실행 중에 크기와 할당/해제 시기가 결정됩니다.

< 스택 메모리의 단점(2) - 크기 >

  • 크기를 컴파일 시에 결정하므로 너무 크게 못잡습니다.
  • 그렇기 때문에 큰 데이터를 처리해야할 겨우 스택 메모리에 못넣습니다.

3️⃣ 힙(heap) 메모리

  • 스택 메모리처럼 특정 용도로 떼어 놓은 것이 아닌 범용적인 메모리입니다.
  • 컴파일러 및 CPU가 자동적으로 메모리관리를 안 해줍니다.
  • 따라서 프로그래머가 원하는 때 원하는 만큼 메모리를 할당받고 해제할 수 있습니다.

< 힙메모리장점 >

  • 스택 메모리처럼 용량 제한이 없습니다.(컴퓨터에 남아있는 메모리기준)
  • 사용자가 데이터의 수명을 직접 제어할 수 있습니다.(오히려 위험할수도..)

< 힙메모리단점(1) >

  • 빌려온 메모리를 직접 해제 안 하면 누구도 그 메모리를 쓸 수 없습니다. 만약 그 메모리 주소를 잃어버리면 메모리 누수(memory leak)이 발생합니다.

< 힙메모리단점(2) >

  • 스택(stack)메모리에 비해 할당/해제 속도가 느립니다.(엄청 느립니다)
  • 스택(stack)메모리가 높은주소부터 오프셋 개념으로 쌓이는데 반해 힙(heap)메모리는 사용/비사용 의 개념으로 메모리를 관리합니다. 그렇기 때문에 메모리 공간에 구멍이 생겨 효율적으로 관리가 어려울 수 있습니다. 이를 메모리 파편화(memory fragmentation)이라고 합니다.

4️⃣ 힙 메모리 취약점(UAF)

  • UAF(use after free)란 할당된 메모리를 사용한 후 free함수로 해제하고 재사용할 경우 일어나는 취약점입니다.
  • 이러한 문제는 메모리할당을 효율적으로 하기 위해, free해제된 영역의 크기를 기억해놨다가 같은 크기의 할당 요청이 들어오면 이전 영역을 재사용하기 때문에 일어납니다.
  • 주소는 물론 그 주소에 저장된 값을 그대로 불러올 수 있기 때문에 보안상으로도 위험한 문제입니다.
  • 하지만 최근에 이런 취약점이 많이 해결된 듯합니다.(밑에 코드참고)

< UAF예제 >

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

int main(void)
{
        char *temp1;
        char *temp2;

        temp1 = (char*)malloc(sizeof(char) * 10);
        
        printf("temp1_address: %p\n", temp1);
        temp1[0] = 'h';
        temp1[1] = 'e';
        temp1[2] = 'l';
        temp1[3] = 'l';
        temp1[4] = 'o';
        temp1[5] = '\0';
        printf("temp1: %s\n", temp1);
        free(temp1);             //temp1메모리 해제
        printf("afterfree_temp1_address: %p\n", temp1); //free후에 기존주소값이 유지

        temp2 = (char*)malloc(sizeof(char) * 10);
        printf("temp2_address: %p\n", temp2);
        printf("temp2: ");
        for(int i = 0; i < 5; i++) // for문으로 각요소값을 확인
                printf("%c", temp2[i]);
        free(temp2);
}

<< 출력값 >

temp1_address: 00651608
temp1: hello
afterfree_temp1_address: 00651608 //free후에도 주소값은 유지
temp2_address: 00651608
temp2: ?e // 다행히 값이 다른값으로 대체되어있습니다

  • 윈도우10 64비트 Vscode환경에서 실험했을 때는 기대했던 UAF상황이 일어나지 않았습니다.(운영체제나 컴파일러가 계속해서 이러한 취약점들을 보완해가는 것 같습니다)
  • free해도 값이 그대로 남아있어서 UAF문제가 생겼던 것인데 free를 함과 동시에 값이 다른값으로 바뀌었습니다. 하지만 주소값은 그대로 유지되었습니다.
  • 하지만 UAF힙오버플로우를 이용한 보안공격의 사례가 있었던 만큼 알아두고 조심하는 것이 좋을 것같습니다.

5️⃣ 레지스터(register)

< 레지스터가 있는 이유? >

  • CPU가 연산할 때마다 메모리에 접근하는 시간이 발생합니다.
  • 대부분 컴퓨터에 장착하는 메모리는 DRAM(dynamic random access memory)입니다. 이 DRAM은 기록된 내용을 유지하기 위해서는 주기적으로 정보를 다시 써야 됩니다.
  • DRAM과 같이 주기적으로 정보를 다시 쓸필요없는 SRAM(static ram)이 있지만 가격이 굉장히 비싸기 때문에 큰용량을 SRAM으로 사용하기에는 부담스럽습니다.
  • 그 대신 CPU내부에 SRAM을 내장시키게 됬습니다.(매우 적은 용량만) 이것이 바로 레지스터입니다.

< 레지스터란? >

  • CPU에서만 사용할 수 있는 고속 저장 공간(가장 빠름)입니다.
  • CPU와 비슷하게 휘발성특성을 가지고 있습니다.
  • CPU가 연산을 할 때 레지스터에 저장되어 있는 데이터를 사용합니다.(보통 그 연산결과도 레지스터에 다시 저장합니다.)
  • 레지스터는 흔히 말하는 메모리가 아닙니다. (보통 CPU가 레지스터에 접근하는 방법과 메모리 접근하는 방법이 다릅니다.)

< 레지스터 사용 >

  • 변수를 레지스터로 사용해달라고 직접요청이 가능합니다.(register키워드 사용)
    register <자료형> <변수명>;
    

< 레지스터 사용 예 >

register size_t num;
register int i;

for (i = 0; i < 100; i++)
    num += i;
printf("num: %d\n", num);
  • 레지스터는 메모리가 아니기 때문에 몇가지 제약이 있습니다.
    1. 변수의 주소를 구할 수 없습니다.
    2. 레지스터 배열을 포인터로 사용할 수 없습니다.
    3. 블록 범위에서만 사용이 가능합니다. (전역 변수에서 사용불가)

< 요즘엔 레지스터를 신경쓸 필요가 없다! >

  • 요즘 데스크톱 컴파일러들은 register 키워드를 넣어준다고 특별히 해주는 일이 없고 무시합니다.
  • 레지스터를 직접 관리하는 것은 예전 임베디드 시스템에서만 의미가 있었습니다.
    • CPU가 매우느렸고
    • 메모리 용량도 적었고
    • 결정적으로 컴파일러가 최적화를 잘 해주지 않았기 때문에 사용자가 직접관리했습니다.
  • 이제는 컴파일러베포(release)모드에서 알아서 최적화 해줍니다.
  • 그렇기 때문에 register(레지스터)키워드는 더 이상 사용자가 수동으로 사용하는 키워드가 아닙니다.




© 2021.02. by kirim

Powered by kkrim