Sqix

64bit 멀티코어 OS 제작하기 [5] - 1 : 세그먼트 디스크립터 본문

MINT64 OS

64bit 멀티코어 OS 제작하기 [5] - 1 : 세그먼트 디스크립터

Sqix_ow 2018. 6. 4. 05:27

이 글은 세그먼트 디스크립터에 대해서 다룹니다.


16비트에서 32비트 모드로 전환하기 위해서는 프로세서에서 참조하는 GDT와 세그먼트 디스크립터라는 자료구조를 생성하고, 프로세서에 이를 설정하여야 합니다. 32비트 모드로 전환하기 위해서는 다음과 같은 과정을 거칩니다.


(출처 : http://getchabug.blogspot.kr/2016/02/64-bit-os-production-01switch-to-32bit.html)


전환 과정은 위 그림과 같습니다. 우선, 두 자료구조인 세그먼트 디스크립터와 GDT를 생성합니다. 그 후, GDTR에 GDT Start Addr, GDT Size를 설정합니다. 32비트로 전환하기에, CR0 Register의  PE 비트를 1로 세팅하고, PG 비트를 0으로 세팅하여 32비트를 사용함을 선언해 주어야 합니다. 그 후, CS 세그먼트 셀렉터를 설정하고 동시에 EIP 레지스터를 설정해 줍니다. 해당 명령어는 jmp dword 0x08: (PROTECTED - $$ + 0x10000)로, 자세한 것은 추후 다루도록 하겠습니다. 이 과정을 거치면, 32비트 모드에서 사용할 각종 세그먼트 셀렉터 및 스택을 초기화합니다. 모든 과정을 거치면 비로소 32비트 커널을 실행할 수 있게 됩니다.


이제 첫 단계인 세그먼트 디스크립터에 대해서 먼저 알아봅시다.


(출처 : https://www.intel.co.kr/content/www/kr/ko/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html)


세그먼트 디스크립터는 GDT와 LDT의 자료구조로, 프로세서에게 세그먼트의 크기와 위치를 제공하여 접근 제어 및 상태 정보 확인을 할 수 있도록 합니다. 세그먼트 디스크립터는 일반적으로 컴파일러, 링커, 로더, OS, 혹은 어플리케이션이 아니지만 실행 가능한 프로그램들로 인해 생성됩니다. 


세그먼트 디스크립터의 각 필드 및 플래그들은 다음을 의미합니다.


세그먼트 제한 필드(Segment Limit)

세그먼트의 크기를 설정합니다. 프로세서는 20비트의 세그먼트 리밋 필드를 쓰기 위해 두 필드를 합쳐서 사용합니다. G 플래그에 따라서 세그먼트 제한을 둡니다.

- 만약 G(granularity, 가중치) 필드가 0이라면, 1KB~1MB까지 설정이 가능하고, 1KB씩 증가합니다.

- 만약 G 필드가 1이라면, 4KB~4GB까지 설정 가능하며, 4KB씩 증가합니다.

- 프로세서는 세그먼트가 expand-up / expand-down인지에 따라 두 가지 기능을 합니다. Expand-up 세그먼트는 유효한 주소인지 아닌지를 정의하고, 허용되는 Base Offset과 최대 가능 offset을  정의합니다. 반면, Expand Down은 이를 뒤집는데, 정의된 유효 주소와 그렇지 않은 주소를 뒤바꿉니다.


베이스 주소 필드(Segment Base Address)

3개의 베이스 주소 필드를  합쳐 하나의 32비트 값을 정합니다. 이는 4GB의 선형 주소 공간 안에서 세그먼트의 0byte 위치를 지정해 줍니다. 세그먼트 베이스 주소는 16바이트로 정렬되어야 합니다. 이렇게 정렬을 해 준다면 코드 효율이 극대화됩니다.



세그먼트 타입(Segment Type)

세그먼트 디스크립터가 시스템 세그먼트인지(S Flag가 0), 코드 혹은 데이터 세그먼트인지(S Flag가 1)를 결정합니다.


디스크립터 권한 레벨 필드(Descriptor Privilege Level Field)

권한 레벨을 설정합니다. 0에서 3의 범위를 가지고, 0은 최고 권한을 의미합니다. C(Current)PL과 R(Requested)PL을 이용하여 DPL을 결정합니다. 


유효 세그먼트 체크 Flag(Segment Present, P flag)

현재 세그먼트가 메모리에서 유효한지(P Flag가 1), 유효하지 않은지(P Flag가 0)를 검사합니다. 플래그가 만약 0이라면, 세그먼트 셀렉터가 세그먼트 레지스터에 로드된 세그먼트 디스크립터를 가리킬 때 프로세서는 Segment-Not-Present 예외를 생성합니다(#NP).  메모리 관리 소프트웨어는 이 플래그를 통해 가용 시간 동안 어떠한 세그먼트를 물리 메모리에 로드할 지 결정합니다. 해당 플래그는 이외에도 가상 메모리에 대한 페이징을 관리할 수 있도록 합니다.


기본 동작 크기(16 / 32bit) / 기본 스택포인터 크기 결정 플래그 (D/B Flag)

16비트 세그먼트로 동작하는지(D/B Flag가 0), 32비트 세그먼트로 동작하는지(D/B Flag가 1)를 결정하는 플래그입니다. 해당 플래그는 세그먼트 디스크립터가 Executable Code Segment인지, Expand-Down Data Segment인지, Stack Segment인지에 따라 다른 동작을 수행합니다. 

- Executable Code Segment인 경우, D Flag라고 불리우며 세그먼트의 명렁어로 참조되는 유효한 주소와 오퍼랜드의 기본 길이를 지정합니다. 1로 지정되는 경우 32비트 주소와 32비트 혹은 8비트의 피연산자가 지정됩니다. 0으로 세팅되면 16비트 주소와 16비트 혹은 8비트의 피연산자가 지정됩니다.

- Stack Segment(Data Segment가 SS 레지스터에 의해 가리켜지는 경우)인 경우, B 플래그라고 불리우며 스택 연산자(push, pop, call 등)에 사용되는 스택 포인터의 크기를 지정합니다. 플래그가 1로 세팅되면 32비트의 스택 포인터(ESP)가 사용되고, 0으로 세팅되면 16비트의 스택 포인터(SP)가 사용됩니다. 만약 Expand-down 상태의 스택 세그먼트인 경우, 이와 더불어 스택 최대 크기도 지정해 줍니다.

- Expand-Down Data Segment인 경우, B Flag라고 불리며 1인 경우 스택의 최대 크기가 0xFFFFFFFFH(4GB), 0인 경우 스택의 최대 크기가 0xFFFFH(64KB)로 세팅됩니다.


세그먼트 단위 플래그(G flag, Granularity)

세그먼트 제한 필드의 검사 단위를 결정합니다. G Flag가 0이라면 byte 단위로 해석되고, 1이라면 4KB 단위로 해석됩니다.1로 세팅되면 12개의 최하위 비트가 세그먼트 제한 검사를 할 때 테스트되지 않습니다. 


64비트 코드 세그먼트 플래그(L flag)

IA-32e 모드에서, 세그먼트 디스크립터의 두 번째 Dword의 21번째 비트에 포함된 플래그로 코드 세그먼트가 Native 64bit code를 포함하는지를 판단하게 해 주는 플래그입니다. 1로 세팅되면 D-bit가 0으로 결정되고 64비트 모드의 코드 세그먼트임을 나타냅니다. 0으로 세팅되면 IA-32e 모드의 32비트 호환용 코드 세그먼트임을 나타냅니다.


Available and Reserved bits

세그먼트 디스크립터의 두 번째 Dword듸 20비트 부분으로, OS에 의해 임의로 사용될 수 있는 영역입니다.


(출처 : https://www.intel.co.kr/content/www/kr/ko/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html)


이제 위 데이터를 바탕으로 세그먼트 디스크립터의 각 필드 값을 지정해야 합니다. 우선 코드 세그먼트 스크립터와 데이터 세그먼트 스크립터의 타입을 지정해 봅시다. 이는 S Flag와 Segment Type 필드를 사용합니다. S Flag가 1인 경우, 디스크립터는 코드 혹은 데이터 세그먼트 모두의 상태가 될 수 있습니다. 이 경우 타입 필드의 11번쨰 비트가 코드 세그먼트(1)인지, 데이터 세그먼트(0)인지 결정합니다. 데이터 세그먼트인 경우 하위 3비트(10,9,8)는 각각 접근 가능 여부(8), 쓰기 권한 여부(9), 확장 방향성(10, 1인 경우 역방향 확장(Expand-down), 0인 경우 정방향 확장(Expand-Up)를 나타냄)을 결정합니다. 코드 세그먼트인 경우 하위 3비트는 각각 접근 가능 여부(8), 읽기 가능 여부(9), 접근 승인 여부(10, 권한과 상관없이 접근할 수 있는지에 대한 여부)를 결정합니다. 커널에서는 기본적인 세그먼트 타입을 사용할 것이고, 이는 코드의 경우 0x0A(Executable / Read), 데이터의 경우 0x02(R/W)입니다. 


타입 지정이 끝났으면, 세그먼트의 최대 크기를 설정할 것입니다. 커널 세그먼트 디스크립터는 4GB 전체에 접근하도록 할 것이기 때문에 G Flag를 1로 세팅합니다. 

기본 오퍼랜드의 크기는 보호 모드이기 때문에 32비트로 지정합니다. D/B 필드와 L 필드가 이를 담당하는데, 32비트 모드이기 때문에 D/B 필드는 1, L 필드는 0으로 설정하여 32비트를 맞춰 줍니다. 


이제 권한 설정을 해야 합니다. 32비트 OS라면 권한을 정확히 부여해야 하지만, 우리는 이를 64비트 전환을 위해 사용할 것이므로 권한 설정을 따로 구분하지 않기 떄문에 0(최상위)으로 지정해 줍니다. 또한, 보호 모드 전환 시 해당 세그먼트가 필요하므로 유효성을 검증하는 P Flag를 1로 설정합니다. 32비트 모드에서는 따로 추가 필드를 사용할 것이 아니기 때문에 AVL 필드 사용 여부는 0으로 설정합니다.


최종적으로 필드의 값을 정리해 봅시다.


코드 디스크립터


Limit : 0xFFFF [15 : 0]

Base : 0x0000 [15 : 0] / 0x00 [23 : 16]

P : 1

DPL : 0

S : 1

TYPE : 0x0A

G : 1

D : 1

L : 0

AVL : 0

Limit : 0xF [19;16]

Base : 0x00 [31 : 24]


데이터 디스크립터


Limit : 0xFFFF [15 : 0]

Base : 0x0000 [15 : 0] / 0x00 [23 : 16]

P : 1

DPL : 0

S : 1

TYPE : 0x02

G : 1

D : 1

L : 0

AVL : 0

Limit : 0xF [19;16]

Base : 0x00 [31 : 24]





이제 어셈블리 코드로 자료구조를 생성해 줄 차례입니다. 세그먼트 디스크립터는 메모리 공간에 위와 같이 올라가게 됩니다. 즉, 상위 부분부터 그대로 올라갑니다. 반면, 어셈블리 코드는 스택 형식으로 위에 작성한 코드가 아래부터 적용됩니다. 따라서 이에 맞추어 실제 디스크립터 생성 코드를 작성하여야 합니다.


코드는 다음과 같습니다.


CODEDESCRIPTOR:

dw 0xFFFF    ; Limit [15 : 0 ]

dw 0x0000    ; Base [15 : 0 ]

db 0x00        ; Base [23 : 16 ]

db 0x9A        ; P = 1, DPL = 0, Code Segment, Execute/Read

db 0xCF        ; G = 1, D = 1, L = 0, Limit [ 19 : 16 ]

db 0x00        ; Base [ 31 : 24 ]


DATADESCRIPTOR:

dw 0xFFFF    ; Limit [15 : 0 ]

dw 0x0000    ; Base [15 : 0 ]

db 0x00        ; Base [23 : 16 ]

db 0x92        ; P = 1, DPL = 0, Code Segment, Read / Write

db 0xCF        ; G = 1, D = 1, L = 0, Limit [ 19 : 16 ]

db 0x00        ; Base [ 31 : 24 ]


지금까지 세그먼트 디스크립터에 대해 알아보았습니다. 이제 이를 이용해서 GDT 자료구조를 생성하여 보도록 하겠습니다. 

Comments