Digital Forensics/Memory

#2 메모리 - 가상 메모리

geunyeong 2023. 8. 19. 23:30

Table of Contents

    Memory Management

    Components of Process

    실질적인 실행코드의 실행은 스레드가 담당한다. 프로세스는 이러한 스레드가 동작하는데 필요한 관련 데이터들의 집합이자 이 집합의 표현이다. 스레드가 동작하기 위해서는 생각보다 많은 구성요소가 필요하다.

    • 스레드 (단순히 코드를 실행하는 것 뿐만 아니라 스레드의 우선순위 등도 포함된다)
    • 메모리 (실행코드를 저장할 영역 뿐만 아니라 스택, 힙 등 저장해야될 데이터가 많다)
    • 세마포어
    • 이벤트
    • 보안 토큰
    • 모듈 (EXE, DLL 같은 모듈)
    • 리소스
    • 명령 파라미터
    • 기타 등등...

    프로세스는 위 구성요소들을 모두 관리하고, 이들을 대표하여 시스템이나 사용자에게 표현한다. 작업관리자나 Process Hacker 등으로 프로세스를 확인하는 행태가 프로세스의 표현을 눈으로 관찰하는 것이라고 할 수 있다. 그래서 프로세스를 확인하면 각 프로세스를 고유하게 식별할 수 있는 프로세스 ID나 실행된 프로그램의 경로, 명령 파라미터 등을 볼 수 있다. 운영체제는 프로세스의 구성요소들을 저장한 구조체(Windows: _EPROCESS, Linux: struct task_struct)로 프로세스들을 관리한다.

    Memory Management Techniques

    운영체제는 프로세스를 관리하는 프로세스다. 컴퓨터 주변장치(마우스, 키보드, 마이크 등)에서 발생하는 입력신호를 처리하거나 프로세스의 스레드가 잘 실행될 수 있도록 프로세스에게 메모리를 할당하고 해당 스레드가 CPU에 의해 실행될 수 있도록 차례를 부여한다. 여기서 주목할 것은 프로세스가 사용할 메모리를 할당한다는 점이다.

    메모리는 한정된 자원이다. 조금 싼 데스크톱은 8GB, 노트북은 4GB 정도의 램이 장착되고, 조금 값을 올리면 16GB도 볼 수 있다. 스펙업에 열광하는 사람들은 개인 컴퓨터에 램 128GB를 장착하기도 한다. 보통 16GB 정도는 되어야 큰 불편 없이 쓸 수 있다고 하는데 프로세스가 사용하는 메모리 양은 얼마나 될까? 우리가 제일 많이 사용하는 크롬의 메모리 사용량을 확인해보면 말도 안되게 큰 걸 볼 수 있다.

    크롬 프로세스 하나만 해도 2TB를 쓰고 있는데, 현재 실행중인 크롬 프로세스는 8개다

    물론 크롬이 무거워진 것도 있지만 2TB에 달하는 메모리를 사용하고 있는 걸 볼 수 있다. 심지어 메모리 사용량이 2TB인 크롬이 6개, 3TB인 크롬이 2개나 있다. 도합 18TB다. 참고로 필자의 컴퓨터에 장착된 메모리는 32GB다. 

    컴퓨터에 장착된 메모리보다 수십배나 더 큰 메모리를 사용하고 있는데 컴퓨터는 어떻게 크롬을 원활하게 실행할 수 있는 걸까? 프로세스가 컴퓨터에 장착된 메모리보다 더 큰 공간을 사용할 수 있도록 하고, 더불어 서로 다른 프로세스 간 메모리를 침범하지 않도록 하기 위해 운영체제는 다양한 메모리 관리 기법을 사용한다. 대표적으로 가상 메모리와 페이징이 있다.

    Virtual Memory

    가상 메모리는 각 프로세스 마다 고유한 가상의 메모리 공간을 할당하는 기법이다. 가상 메모리의 목적은 아래와 같다.

    • 컴퓨터에 장착된 메모리 용량보다 더 큰 메모리를 사용할 수 있도록 한다
    • 서로 다른 프로세스 간에 메모리 공간을 침범하지 않도록 한다

    Virtual Address Space

    모든 프로세스가 접근하는 메모리 공간은 가상 주소 공간이다. 가상 주소 공간이란 가상 주소로 접근할 수 있는 각 프로세스의 고유한 메모리 공간을 의미한다. C/C++ 언어를 사용할 때 주소값을 출력하는 "&"(앰퍼샌드) 연산자를 써본 적이 있을 것이다. 앰퍼샌드 연산자를 통해 출력한 주소값이 바로 가상 주소다. 프로세스는 오로지 가상 주소만을 사용해 메모리에 접근해야 한다.

    Windows에서의 가상 메모리 모습 (커널 영역은 모든 프로세스에게 고유하지 않고 공유된다)

    가상 주소 공간은 각 프로세스마다 고유하다. A라는 프로세스가 가상주소 0x1234`5678 번지에 100이라는 정수값을 넣고 하고 B라는 프로세스가 마찬가지로 가상주소 0x1234`5678 번지에 200이라는 정수값을 넣는다면 두 저장 작업은 평행세계처럼 서로에게 아무런 영향을 미치지 않는다. A가 0x1234`5678에서 값을 읽어오면 100이 반환되고, B가 가상주소 0x1234`5678에서 값을 읽어오면 200이 반환된다. 주소는 같을지언정 서로 다른 세계다.

    64비트 프로세스 기준으로 가상 주소 공간의 크기는 256TB다. 32비트 프로세스는 4GB가 최대다. 여기서 말하는 n비트는 주소를 표현할 수 있는 크기를 의미한다. 64비트면 2^64까지 주소를 표현할 수 있다는 의미이며, 2^64 바이트는 256TB에 해당한다. 하지만 이렇게나 큰 공간을 쓸 일은 거의 없기 때문에+성능이슈로 인해 사용자 모드 영역을 표현하는데 64비트 중 48비트만을 사용한다(0x0000`0000`0000`0000 ~ 0x0000`7FFF`FFFF`FFFF).

    Virtual to Physical Address Translation

    가상 주소인 이유는 이 주소가 메모리의 실제 주소가 아니기 때문이다. 가상 메모리를 사용하는 시스템에서 메모리 주소는 2가지로 구분된다. 가상 주소와 물리 주소다. 물리 주소는 실제 램에 접근할 수 있는 주소다. 물리주소 0x1234`5678은 실제 램에서 0x1234`5678 오프셋에 해당한다. 즉 실제 물리 메모리에 접근하려면 가상 주소를 물리 주소로 변환해야만 한다.

    가상 주소는 앞서 말했듯 256테라 바이트의 공간도 다룰 수 있다. 하지만 컴퓨터에 장착된 메모리는 끽해야 8GB, 많아야 32GB다. 가상 주소를 그대로 물리 주소로 사용하면 컴퓨터에 장착된 메모리의 범위를 넘어서버릴 수 있다. 그래서 운영체제와 CPU는 프로세스가 참조하는 가상 주소를 물리 주소로 변환하는 기능을 가지고 있다.

    CPU에 탑재된 메모리 관리 유닛(Memory Management Unit; MMU)은 페이지 테이블(Page Table)을 참조해 프로세스가 접근하는 가상 주소를 물리 주소로 변환한다. 이 때 캐시 역할을 하는 것이 TLB(Translation Lookaside Buffer)다. 가상 주소를 물리 주소로 변환하는 정보를 가진 페이지 테이블은 프로세스 마다 존재하기 때문에 서로 다른 프로세스가 같은 가장 주소에 접근해도 변환된 결과가 다르기 때문에 각자만의 고유한 공간을 가진 것처럼 보이는 것이다.

    서로 다른 프로세스에서 동일한 가상 주소를 물리 주소로 변환한 모습 (같은 가상 주소여도 다른 물리 주소로 변환되었다)

    주소 변환의 이점은 또 있다. 공유 메모리처럼 서로 다른 프로세스가 동일한 메모리 공간을 같이 사용해야될 경우 주소 변환의 결과 값이 동일하면 된다. 같은 물리적인 공간을 공유하기 때문에 A 프로세스가 100이라는 정수값을 넣으면 B도 100이라는 정수값을 읽어들일 수 있게 된다. 주소 변환을 통한 공유 메모리는 물리 메모리 공간을 절약하는데에도 사용할 수 있다. 운영체제의 커널 뿐만 아니라 DLL(user32, kernel 등) 같은 모듈도 이를 통해 제공된다.

    Virtual Address Descriptor

    각 프로세스마다 가상 주소 공간은 256TB씩 제공된다. 그런데 아무 주소나 참조하면 세그먼테이션 폴트(Segmentation Fault)가 발생한다. 이는 접근한 가상 주소를 물리 주소로 변환하는데 실패한 것이다. 즉 사용하지 않는 가상 주소라는 뜻이다.

    우리가 malloc이나 calloc으로 사용 가능한 메모리 공간을 할당받는 걸 생각해보자 (malloc과 calloc의 동작원리는 좀 다르지만 말하고 싶은 건 사용 가능한 메모리 공간을 할당받는단 것이다). 사용 가능한 메모리 공간을 할당받는다는 것은 아무 곳이나 접근할 수 있는 게 아니란 말과 일맥상통하다. 할당받지 않은 공간은 접근할 수 없다.

    한 프로세스의 전체 가상 주소 공간 중 사용 가능한(할당받은) 공간들이 회색으로 칠해져있다

    가상 주소 설명자(Virtual Address Descriptor; VAD)는 프로세스가 할당받은 가상 주소 공간 내 메모리 공간을 나열한다. VAD들은 트리 구조로 연결되어 있고, 트리 노드가 VAD다. VAD는 가상 주소 공간의 시작 주소와 끝 주소 뿐만 아니라 해당 공간의 권한(읽기, 쓰기, 실행) 정보도 함께 가지고 있다. 가상 주소가 연속되어 있더라도 할당된 권한정보가 다르면 서로 다른 VAD가 생성된다. 가령 0x1234`0000부터 0x1234`FFFF는 READONLY이고, 0x1235`0000부터 0x1235`FFFF는 READWRITE라면 이들 각각을 나타내는 2개의 VAD가 만들어지고 트리에 추가된다.

    Page & Frame

    파일 시스템에 클러스터가 있다면 메모리 관리에는 페이지와 프레임이라는 것이 존재한다. 클러스터와 마찬가지로 메모리 입출력 성능 향상을 위해 메모리 공간을 일정 크기로 쪼개어 관리하는 것이다. 페이지는 가상 주소 공간을 일정 크기로 나눈 것, 프레임은 물리 메모리 공간을 일정 크기로 나눈 것을 나타내며 일반적으로 둘 다 4KB로 설정된다.

    Page Table

    프로세스의 가상 주소 공간을 물리 메모리 공간으로 매핑하기 위한 정보가 담긴 테이블이다. 64비트 프로세스에서는 트리 구조로 테이블이 구성된다.

    가상 주소는 크게 5가지 영역으로 구분된다. 이중 Physical Page Offset은 물리 프레임인 4KB 내에서의 오프셋을 나타내기 때문에 변환 과정에서 달라지지 않는다. 변환에 필요한 값은 Page로 시작하는 4개 영역이다. 이 4개 영역이 주소 변환 과정에서 물리 메모리 주소로 바뀌게 된다.

    • Page-Map Level-4 Offset (PML4)
    • Page-Directory-Pointer Offset
    • Page-Directory Offset
    • Page-Table Offset
    • Physical Page Offset

    전체 페이지 테이블의 시작 주소는 CR3라는 레지스터에 저장되며 CR3에 저장된 주소는 물리 주소다 (컨텍스트 스위칭 때마다 CR3 레지스터의 값은 갱신된다). PML4 Offset에 해당하는 9개 비트는 오른 시프트(Right Shift) 39 연산을 통해 정수로 변환되고, CR3가 가리키는 물리 주소에서 상대적인 오프셋으로 사용된다.

    "AMD64 Architecture Programmer’s Manual Volume 2: System Programming"에 나온 V-to-P 변환 방법

    만약 A 프로세스의 CR3가 0x3456`0000이고 PML4 Offset 값이 0xDD라면, PML4 Entry(PML4E)의 크기는 8바이트이므로

    PML4 Entry's Physical Addrss = 0x3456`0000 + (0xDD * 8) = 0x3456`0000 + 0x6E8 = 0x3456`06E8

    PDP Table이 저장된 물리 주소는 0x3456`06E8에 저장되어 있다. 0x3456`06E8에서 8바이트를 읽어와 상위 52비트를 가져오면 그 값이 다음 PDP Table 저장된 물리 주소를 가리킨다. PDP와 PD, PT도 모두 같은 방법으로 계산하여 최종적으로 가상 주소를 물리 주소로 변환한다.

    Page Table Entry

    편의상 PML4E, PDPE, PDE, PTE를 모두 지칭한다. 위 그림과 설명에서도 봤다싶이 각 엔트리에서 52비트만 사용하는데, 하위 12비트를 사용하지 않는 이유는 필요가 없기 때문이다. 앞서 말했듯 Physical-Page Offset을 나타내는 하위 12비트는 변환 과정에서 변해선 안되기 때문에 페이지 엔트리에서 하위 12비트는 다른 용도로 사용된다. (어떠한 상태정보를 나타내는 것으로 보이는데 자세한 건 인텔과 AMD의 Programmer Manual을 좀 읽어봐야 할 것 같다.)

    세그먼테이션 폴트는 이렇게 페이지 테이블을 참조하는 과정에서 페이지 테이블 엔트리에 유효한 값이 존재하지 않아 발생한다.

    물리 메모리에서의 페이지(프레임)의 상태를 저장하는 Page Frame Number Database (PFN DB)라는 것이 있다. 이는 후에 페이징에서 더 다루도록 하겠다.