Sqix

64bit 멀티코어 OS 제작하기 [2] - 2 : 메모리 관리 기법 본문

MINT64 OS

64bit 멀티코어 OS 제작하기 [2] - 2 : 메모리 관리 기법

Sqix_ow 2018. 5. 10. 16:00

이번 글에서는 운영 모드와 메모리 관리 기법에 대해서 다룹니다.


x86, x86_64에서는 크게 세그먼테이션(Segmentation), 페이징(Paging) 기법을 통해서 메모리를 관리합니다. 


세그먼테이션은 우리가 전체 영역을 원하는 크기로 분할하여 관리하는 것이고, 페이징 기법은 지정 단위로 잘린 영역을 모아 원하는 크기를 만들어 관리하는 방식입니다.


메모리 관리 기법을 이용하려면 각 레지스터에 맞는 특정한 자료구조를 지정해야 합니다. 세그먼테이션은 디스크립터의 위치를 지정해야 합니다. 그리고 페이징 기법에서는 컨트롤 레지스터의 CR3 레지스터에 페이지 디렉토리라고 불리는 자료구조의 물리적 주소를 설정해 주어야 합니다.


이제 위의 내용을 근간으로 각 운영 모드별 메모리 관리 방식에 대해서 알아보겠습니다.


리얼 모드의 메모리 관리 방식


리얼 모드에서는 메모리 공간이 최대 1MB만큼 사용될 수 있습니다. 또한, 메모리 관리 방식은 세그먼테이션만 지원합니다. 각 세그먼트의 크기는 64KB로 고정되어 있고, 시작 주소는 세그먼트 레지스터에 직접 설정됩니다. 설정된 시작 주소는 코드 및 메모리에 접근할 때 기준 주소로 사용됩니다. 


예를 들어 다음과 같은 물리 주소를 가진 메모리 공간이 존재한다고 가정합시다.

64K Stack : 0x40000 ~ 0x40fff || 64K Data : 0x10000 ~ 0x10fff || 64K Data : 0x2000 ~ 0x2fff || 64K Code : 0x0 ~ 0xffff

그렇다면 세그먼트 레지스터는 다음과 같이 할당됩니다.

SS(스택 세그먼트) : 0x40000 / FS : 0x10000, GS : 0x10000 / ES(Destination Segment) : 0x2000, DS(Data Segment) : 0x2000 / CS(Code Segment) : 0x0


물리적 주소를 결정하는 방법은 간단합니다. 세그먼테이션을 거쳐서 나온 주소가 바로 물리 주소가 됩니다. 여기서 세그먼테이션은 세그먼트 레지스터의 값에 범용 레지스터의 값을 더하는 것으로 결정됩니다. 하지만 그냥 더하는 것은 아니고, 최대 1MB의 주소에 접근하기 위해 세그먼트 레지스터(16bit)의 값에 16을 곱해서 이를 기준 주소로 사용하도록 합니다.


다시 예를 들어, 세그먼트 레지스터가 0x1000, 범용 레지스터가 0x1234의 값을 가진다고 가정해 봅시다.

위에서 말한 대로 세그먼트 레지스터에는 16을 곱하기 때문에, 프로세서에서는 세그먼트 레지스터는 0x10000의 값을 가지게 됩니다. 거기에 범용 레지스터의 값을 더하면 0x11234라는 결과를 얻을 수 있습니다. 그리고 이렇게 나온 결과가 물리 주소 0x11234 값이 됩니다.

즉, "세그먼트 레지스터의 주소" + "범용 레지스터의 값" = "물리 주소" 값


리얼 모드에서 세그먼트 크기가 64KB로 고정된 이유는 레지스터의 크기 때문인데, 16비트 프로세서에서는 레지스터 크기가 모두 16비트이기 때문에 이를 이용해 접근할 수 있는 범위의 크기(0x0000~0xffff)인 64KB가 된 것입니다. 


● 보호 모드의 메모리 관리 방식


보호 모드는 세그먼테이션과 페이징 기법 모두를 지원합니다. 하지만 보호 모드의 세그먼테이션은 리얼 모드의 세그먼테이션과는 다릅니다. 보다 다양한 기능을 지원하죠. 그리고 보호 모드의 세그먼테이션은 위에서처럼 주소를 직접 설정하지 않고, 디스크립터 자료구조의 오프셋을 설정하는 방식으로 바뀌어서 작동합니다. 세그먼트 레지스터 역시 디스크립터를 선택하는 역할을 합니다.


여러 디스크립터 자료구조 중 세그먼트에 대한 정보를 나타내는 것을 세그먼트 디스크립터라고 합니다. 해당 자료구조에는 시작 주소, 크기, Privilege(권한), Type(타입) 등의 정보가 담겨 있습니다. 여기서 권한은 0~3 사이의 값을 가지는데, 이는 세그먼트에 접근하기 위한 최소한의 권한을 말합니다. 디스크립터의 조건을 만족하지 못하면 프로세서는 예외를 발생시킵니다. 


세그먼트 디스크립터의 구조는 다음과 같습니다.


(출처 : https://stackoverflow.com/questions/38427741/what-is-the-relation-between-selector-and-gdt-in-pm)

보호 모드에서 세그먼트 디스크립터들은 GDT(Global Descriptor Table)에 최대 8192개의 연속된 집합으로써 존재하고, 세그먼트 레지스터는 세그먼트 디스크립터를 가리키는데 쓰입니다. GDT 위치와 관련 레지스터는 GDTR 레지스터이고, 이는 16비트의 GDT Size 필드와가  32비트의 Base Address 필드로 구성된 자료구조의 물리적 주소를 넘겨받습니다. 프로세서는 이를 저장했다가 어드레스에 접근할 때 GDT 위치를 찾는 데 이용합니다(GDT 역시 자료구조이기 때문에 GDT의 위치를 빠르게 넘기는 것 역시 중요하기 떄문입니다). 


보호 모드의 주소 계산식은 리얼 모드와 비슷합니다. 다만, 계산의 결과가 바로 물리 주소로 매핑되지 않고 프로세서에 의해 한 번 더 계산이 이루어집니다. 기존 리얼 모드에서와 같이 "세그먼트의 기본 주소" + "범용 레지스터의 값" = "선형 주소(Linear Address)"가 되고, 여기서 프로세서에 의해 페이지 매핑 과정을 거쳐 물리 주소가 됩니다. 


보호 모드에서는 리얼 모드와 달리 세그먼트 크기를 지정해 줄 수 있습니다. 이는 세그먼트 주소에 접근할 때 참조하게 되며, 범용 레지스터의 값 등 기준 주소에 더해질 주소의 크기는 세그먼트 크기보다 작거나 같아야 합니다.


(출처 : http://www.c-jump.com/CIS77/ASM/Protection/W77_0160_intel_addr_cont.htm)



만약 페이징을 사용하지 않는 프로세서라면, 위 그림에서 32-bit linear address라고 되어 있는, 도출된 선형 주소가 곧 32-bit 물리 주소가 됩니다. 하지만, 만약 페이징을 사용한다면 페이징 과정을 거쳐서 선형 주소를 물리 주소로 변형하여 물리 주소를 결정짓습니다. 페이징 과정은 아래 그림과 같습니다.


(출처 : https://b.luavis.kr/os/linux-virtual-mem)



페이징 기법은 메모리를 일정한 단위인 페이지로 나누고, 도출된 선형 주소와 메모리의 실제 물리 주소를 매핑시켜 주는 것을 말합니다. 페이징 기법을 이용하면 메모리 디버깅이 매우 복잡해지지만, 메모리를 기존보다 더 크게 사용할 수 있기 때문에 이를 이용합니다. 또한, 같은 페이지를 각 프로세스에 연결하여 공유되는 메모리를 처리할 수 있고, 반대로 페이지 자료구조를 따로 생성하여 중복되지 않게 연결하여 독립 메모리 공간을 보장시켜 줄 수 있기도 합니다.


페이징 방식은, 보호 모드에서 4가지 방식으로 존재합니다. 물리적 메모리를 4KB 단위로 나누고 선형 주소를 3단계 / 4단계(물리 주소 확장 기능 사용 시)로 나누는 방법들이 있고, 4MB 크기로 나누고 선형 주소를 2단계 / 3단계(물리 주소 확장 기능 사용 시)로 나누는 방법이 있습니다.


여기서 가장 기본적인 방법은 위 그림에 나와 있는 페이징 기법(4KB 단위로 물리적 주소를 나누고 3단계로 나누는 방법)을 따라가는 것인데, 선형 주소를 Dir(디렉토리 부분), Table(테이블 부분), Offset(오프셋 부분)로 나누어 관리하는 방식입니다. 이는 생성된 선형 주소의 정보를 이용하는 것으로, 선형 주소의 디렉토리, 테이블 부분은 각각 페이지 디렉토리와 테이블(GDT의 자료구조들)의 Entry Point를 나타냅니다. 마찬가지로, 이를 사용하려면 위치를 지정해야 하는데, 여기서는 CR3 Control Register가 GDTR과 같은 역할을 합니다.




(출처 : Osdev Wiki)


페이지 디렉토리, 페이지 테이블 엔트리는 모두 4byte의 크기를 갖습니다. 


* 페이지 디렉토리 엔트리

- S는 페이지 사이즈로, 특정 엔트리에 대한 페이지의 크기를 저장합니다. 해당 비트가 Set되어진다면 페이지는 4MiB의 크기를, 세팅되어있지 않다면 4KB의 크기를 갖습니다.


* 페이지 테이블 엔트리

- G는 글로벌 플래그로, CR3 필드가 리셋이 되는 경우 TLB(변환 색인 버퍼, 가상 메모리 주소를 물리 주소로 변환하는 데 보다 빠르게 해 주는 캐시)를 준비하도록 하는 필드입니다. 단, CR4 비트가 세팅되어있는 경우 이를 이용할 수 있습니다.

- D는 Dirty Flag로, 페이지가 쓰이고 있는지를 알려주는 플래그입니다. 이 플래그는 CPU가 업데이트하지 않고, 한 번 세팅이 완료되면 스스로 비트 세팅을 해제할 수 없습니다.


* 공통

- A는 페이지가 읽기 혹은 쓰기에 사용되었는지를 감별합니다. 비트가 세팅되어지면 O, 그렇지 않다면 X가 됩니다. CPU에 의해 지워지는 것이 아니기 때문에 해당 비트로 인해 OS에 부담이 가지 않습니다.

- D는 페이지의 캐싱이 가능한지 불가능한지를 알리며, 1이 불가능, 0이 가능함을 의미합니다.

- W는 Write - Through, 우리 말로는 연속 기입 방식(데이터가 메모리와 캐시에 동시에 저장되는 것)이 사용될 수 있는지 없는지를 설정하는 비트입니다. 세팅되어지면 사용이 가능한 것이고, 아니라면 불가능한 것입니다.

- U는 유저 / 관리자 권한을 나누는 필드로 비트가 세팅되면 모든 권한에서 페이지를 엑세스할 수 있고, 그렇지 않다면 권한 레벨 2 이하에서만 페이지를 엑세스할 수 있게 됩니다. 이를 이용하여 커널 영역과 유저 영역을 구분하고, 보호할 수 있습니다.

- R은 읽기 / 쓰기 권한과 관련된 플래그입니다. 플래그가 세팅되면 해당 페이지는 R/W가 가능하다는 것이고, 그렇지 않다면 읽기 전용의 권한만 부여 가능한 상태가 됩니다. 또한, CR0 비트를 이용해서 사용자 영역에만 쓰기 권한을 줄 지, 커널에만 쓰기 권한을 줄 지, 혹은 모두에게 권한을 줄 지에 대해서 설정할 수 있습니다.

- P는 Present로, 이 비트는 페이지가 물리적 메모리에 있는지에 대한 것을 알려줍니다. 만약 페이지가 메모리상에 존재하지 않지만 페이지가 호출되는 경우 OS는 이를 핸들링해 주어야 합니다.


(출처 : UIC University - Computer Science : Operating System)


페이징 과정 그림에 나타난 것처럼, Linear Address는 디렉터리(global, middle), 테이블, 오프셋이 나뉘어 있고, 각각 10비트, 10비트, 12비트를 가집니다. 이는 페이지 디렉토리, 페이지 테이블의 Entry가 총 2^10개, 페이지 오프셋이 최대 2^12의 값을 가진다는 것을 나타냅니다. 


페이징을 통해 Linear Address(선형 주소)에서 Physical Address(물리 주소)를 구하는 방법은 다음과 같습니다.

- CR3 레지스터는 메모리 관리를 위한 페이지의 시작점을 가지고 있는 레지스터입니다. 여기서 페이지 디렉토리의 시작 주소를 알아냅니다.

- 페이지 디렉토리의 주소를 알아냈으면, 선형 주소의 시작 주소로부터 10비트의 값(31bit ~ 22bit == offset)을 페이지 시작 주소에 더하여 페이지 테이블의 시작 주소를 알아냅니다.

- 페이지 테이블의 시작 주소를 알아냈으면, 선형 주소의 21bit ~ 12bit의 값을 이용하여 페이지 테이블의 offset을 알아냅니다. 페이지 테이블의 시작 주소 + offset을 통해 알아낸 주소에 있는 값이 4KB짜리 페이지의 시작 주소입니다.

- 이제 페이지의 시작 주소와 선형 주소의 11~0번째 비트(offset)를 더하여 실제 물리적 주소로 변환합니다.


이렇게 페이징 기법을 이용하여 선형 주소를 물리 주소로 변환할 수 있습니다.


IA-32e 모드의 메모리 관리


※ 여기선 IA-32e 모드의 서브모드 중 64bit mode에 대해서만 다룹니다.


64bit mode이기 때문에, 메모리 사용 가능 공간은 기존 2^32에서 2^64로 어마어마하게 늘어났습니다. 약 10억 배 정도로 늘었다고 볼 수 있습니다. 


IA-32e모드의 메모리 관리는 32bit mode(보호 모드)와 크게 다르지 않습니다. IA-32e 모드에서는 기준 주소와 상관없이 모두 세그먼트의 기준 주소가 0, 크기는 64bit로 설정됩니다. 따라서 OS를 설계할 때 64bit는 기준 주소가 세그먼트를 구분할 수 없다는 것을 고려해서 설계해야 합니다.


또한, 세그먼트 디스크립터에 bit 21인 L 필드가 추가되었다는 점이 있습니다. 이는, 서브모드 구분을 위한 것인데 64비트 모드와 호환 모드를 구별하는데 이용됩니다. 1로 세팅되면 64비트 모드, 0으로 세팅되면 호환 모드로 동작합니다. 


(출처 : http://byuljong.tistory.com/85)



IA-32E 모드에서는 기본적으로 64비트 주소 공간을 갖기 때문에 PAE가 자동으로 활성화됩니다. 여기서 PAE란 Physical Address Extension이라고 하는 물리 주소 확장 모드입니다. 이는 4GB 이상의 메모리를 32bit mode에서 사용할 수 있도록 하는 기능을 제공합니다. 주소 공간 역시 확장되었기 때문에 4KB Page는 5단계, 2MB page는 4단계의 변환 과정을 거칩니다. 


우리가 구현하고자 하는 OS는 4kb 5단계 페이징 과정을 가집니다.


(출처 : https://twitter.com/danluu/status/806048521417728000)


※ 48bit부터 63bit는 부호 확장에 사용됩니다.

- CR3 레지스터에서 PML4의 Entry를 찾고, PML4 영역(39~47bit)의 9비트를 offset으로 삼아서 디렉토리 포인터 엔트리를 구합니다.

- 구해진 디렉토리 포인터 기준 주소에 선형 주소의 디렉토리 포인터(30~38bit)의 9비트를 offset으로 삼아서 디렉토리 엔트리를 구합니다.

- 구해진 디렉토리의 엔트리 기준 주소에 테이블(12~20bit)의 9비트를 offset으로 삼아서 4KB 페이지의 기준 주소를 찾습니다.

- 페이지의 기준 주소에 오프셋 영역(0~11bit)의 12비트를 더해 최종 물리 주소를 구합니다.


이렇게 IA-32e 모드의 페이징은 보호 모드의 페이징 방식과 크게 다를 바 없습니다. 조금 다른 점은, Table Index가 각각 10비트에서 9비트로 줄어들어 Entry point의 개수가 512개(2^9)로 줄어들었다는 것입니다. 



(출처 : http://slideplayer.com/slide/4977568/)


또한, 여기서는 48비트의 영역을 사용하는데 그렇다고 해서 48비트 전체가 물리 주소로 변환되는 것은 아닙니다. 위 그림을 보면 알 수 있듯, 0~39비트까지 정상적으로 이용되고(총 40비트), 40~51비트 영역은 항상 0으로 예약되어 있습니다. 또한, 52부터 62비트까지는 사용자 정의 기능을 위해 할당되어 있고, 63비트는 페이지에서 명령어 실행 기능을 활성화 / 비활성화 해주는 비트입니다(이 비트를 이용하여 OS를 보다 안전하게 할 수 있습니다). 즉, 40비트만을 이용하여 최대 1TB(2^40)의 메모리 주소 공간을 가질 수 있다는 것입니다.


이상으로 16, 32, 64비트 모드의 메모리 관리 기법에 대해 알아보았습니다. 다음 글 부터는 실제로 OS를 제작하고 ASM 코딩을 진행하면서 진행하여 보겠습니다.


다음 글은 부트 로더에 관련된 글입니다.

Comments