Sqix

64bit 멀티코어 OS 제작하기 [5] - 2 : 16bit에서 32bit로 전환하기 본문

MINT64 OS

64bit 멀티코어 OS 제작하기 [5] - 2 : 16bit에서 32bit로 전환하기

Sqix_ow 2018. 6. 4. 11:09

이 글은 16비트에서 32비트로 전환하는 과정에 대해 다룹니다.



보호 모드로 동작하는 경우, 모든 메모리 엑세스는 GDT 혹은 optional LDT를 거쳐가게 됩니다. 여기서 GDT란, Global Descriptor Table의 약자입니다. GDT는 세그먼트의 크기, 베이스 주소, 권한 등을 담은 테이블입니다. GDT에 대한 정보는 CPU 내의 GDTR 레지스터에 세그먼트 디스크립터로 저장됩니다. LDT는 GDT와 같이 디스크립터를 포함하는 테이블입니다. 이는 GDT에 포함 가능한 디스크립터가 8192개로 경우에 따라 디스크립터가 모자랄 수 있기 때문에 만들어진 테이블입니다. 


이 테이블은 이전 글에서 언급되었던 세그먼트 디스크립터를 가지고 있습니다. 세그먼트 디스크립터는 세그먼트의 Base Address를 접근 권한, 타입, 사용 정보에 따라 제공합니다. 각 세그먼트 디스크립터는 세그먼트 셀렉터와 연관되어 있습니다. 세그먼트 선택자는 소프트웨어에게 GDT / LDT 사용 여부, Global flag / Local flag 사용 여부, 그리고 접근 권한에 대한 정보를 제공합니다.


세그먼트의 바이트에 접근하고자 하면, 세그먼트 선택자와 오프셋이 제공되어야 합니다. 세그먼트 선택자는 GDT 혹은 LDT에 있는 세그먼트 선택자에 대한 엑세스를 제공합니다. 세그먼트 디스크립터로부터 프로세서는 선형 주소 공간의 세그먼트 베이스 어드레스를 제공받습니다. 오프셋은 기본 주소에 대한 상대적인 위치를 나타내어 줍니다. 만약 세그먼트에 CPL 수준에서 엑세스가 가능하다면 해당 메커니즘을 이용하여 유효한 코드, 데이터, 스택 세그먼트에 접근할 수 있습니다. 


※ 점선은 물리적 주소를, 진한 실선은 신형 주소를, 연한 실선은 세그먼트 셀렉터를 포함합니다.

※ MINT64에서는 태스크 스위칭을 HW 서비스가 아닌 SW 서비스로 해결하기 때문에 태스크 증가량이 많지 않고, 따라서 GDT만을 사용합니다.


간단하게 GDT에 대해 알아보았습니다. 이제 GDTR을 정의하고 GDT테이블을 생성하여 보도록 하겠습니다. 


GDTR은 GDT를 담는 자료구조입니다. GDT를 담는 자료구조의 Base Address는 32비트이고, Address 0을 기준으로 삼는 Linear Address입니다. 현재 코드는 부트로더에 의해 0x10000에 로드되어 실행되고 있고, 여기에 현재 실행되고 있는 섹션의 GDT 오프셋을 더하여 GDT Table의 시작 주소를 구할 수 있습니다. 


또한, GDT 테이블은 기존 Data, Code Descriptor에 초기화를 위한 Null Descriptor를 추가하여 완성하여 주면 될 것입니다.


GDTR:

dw    GDTEND - GDT - 1

dd    ( GDT - $$ + 0x10000 )


GDT :

NULLDESCRIPTOR:

dw    0x0000

dw    0x0000

db    0x00

db    0x00

db    0x00

db    0x00


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 ]

GDTEND:


GDT를 완성하였습니다. 이제 다음 단계인 프로세서에 GDT 정보 설정, CR0 레지스터 설정, jmp 명령 수행을 진행하게 된다면 보호 모드로 전환할 수 있습니다. 


순서대로 진행하여 보겠습니다. 프로세서에 GDT 정보를 설정하는 것은 간단합니다. 인텔 아키텍쳐의 특수 명령어인 lgdt를 이용하면 됩니다.


lgdt [ GDTR ]


해당 명령어는 GDTR 자료구조를 프로세서에 설정하는 어셈블리 명령어입니다. 이 외에도 LLDT, LIDT를 이용해서 LDT, IDT를 설정해 줄 수 있습니다.


이제 CR0 레지스터 설정을 진행하여 봅시다. CR0 레지스터는 운영 모드와 프로세서 상태를 결정짓는 시스템 컨트롤 플래그를 포함하는 레지스터입니다. 여기에는 캐싱, 페이징, 부동소수점 연산장치(FPU) 관련 필드가 포함되어 있습니다. 각 필드들의 의미를 알아보고, 어떤 필드들을 활성화할지 결정하여 보도록 합시다.


PG (Paging, bit 31)

플래그가 1이 되면 페이징 기능을 활성화합니다. 0이 되면 페이징을 비활성화합니다. PG 플래그는 PE 비트가 세팅되어 있지 않으면 동작을 명확하게 수행할 수 없습니다. 만약 PG 플래그를 세팅하고 PE 플래그를 세팅하지 않으면 General-Protection-Exception (#GP)을 발생시킵니다.


CD (Cache Disable, bit 30)

CD 플래그와 NW 플래그가 0이면, 프로세서 내부 및 외부의 전체 물리적 메모리에 대한 캐싱이 허용됩니다. CD 플래그가 1이 되면 캐싱이 제한됩니다. 프로세서가 캐시에 접근하고 이를 업데이트하는 것을 제한하고자 하면 CD 플래그를 활성화해 주면 됩니다. 캐싱 제한은 다음과 같습니다.


NW (Not Write-Through, bit 29)

NW와 CD 플래그가 0인 경우, 캐시 히트 기능과 사이클 무효화 기능을 지원하는 write-back(펜티엄 시리즈, 제온 시리즈)기능과 write-through(486 프로세서)기능이 활성화됩니다. 역시 위 표를 참조하면 해당 기능들에 대해 자세히 알 수 있습니다.


AM (Alignment Mask, bit 18)

AM 플래그가 1인 경우 어드레스 자동 정렬 검사가 활성화됩니다. 어드레스 자동 정렬 검사 기능은 AM 플래그와 EFLAGS의 AC 플래그가 세팅된 상태에서 CPL(Current Privilege Level)이 3이고, 프로세서가 보호 모드 혹은 virtual-8086 모드로 동작 중인 경우 수행이 가능합니다.


WP (Write Protect, bit 16)

WP 플래그가 1인 경우 관리자 권한의 프로시저가 읽기 전용 페이지에 쓰기를 할 수 없도록 합니다. 0인 경우 관리자 권한의 프로시저가 읽기 전용 페이지에 쓰기를 할 수 있도록 합니다. 이 플래그는 유닉스와 같은 운영 체제에서 새로운 프로세스를 만드는(Fork 작업을 하는) copy-onwrite 메소드의 구현을 용이하게 만들어 줍니다.


NE (Numeric Error, bit 5)

NE 플래그가 1인 경우 X87 FPU 에러에 대한 리포팅 메커니즘을 내부 인터럽트로 처리하도록 지정합니다. 0인 경우 PC-Style의 외부 인터럽트로 처리하게 합니다. 만약 NE 플래그가 0이고 IGNNE#가 입력되면, X87 FPU 에러는 무시됩니다. 입력이 되지 않는다면 마스킹되지 않은 X87 FPU Error에 대해서 프로세서는 FERR# pin을 Assert하여 외부 인터럽트를 생성하고 다음 부동소수점 명령어나 WAIT / FWAIT 명령어가 등장하기 전까지 현재 명령어를 실행 중단하고 대기합니다.

FERR# pin은 외부 인터럽트 컨트롤러로의 입력을 위한 핀입니다. NE Flag, IGNNE# pin, FERR# pin은 외부의 로직을 이용하여 PC-style의 에러 리포팅을 하는 데 이용됩니다. 현대의 OS에서는 FERR#과 IGNNE#를 이용하여 에러 처리를 하지 않는 경향이 있습니다. 


ET (Extension Type, bit 4)

FPU를 지원한다는 것을 나타내는 플래그로, 펜티엄 4, 제온, P6 계열, 그리고 펜티엄 프로세서에 제공되는 플래그입니다. 펜티엄 4, 제온, P6 계열에서는 1로 하드코딩되어 있습니다. 386과 486 프로세서에서는 387DX math coprocessor 명령어를 지원하는 경우 1로, 그렇지 않다면 0으로 지정됩니다.


TS (Task Switched, bit 3)

EM, MP 플래그와 조합하여 부동소수점 연산 관련 태스크 스위칭을 지원합니다. TS 플래그가 0로 세팅되면 X87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 컨텍스트가 태스크 스위칭을 하여 지연이 발생하는 경우 새로운 태스크가 생성되면 바로 실행할 수 있도록 컨텍스트 상태를 저장하고 복구하는 기능을 허용하도록 합니다. 프로세서는 해당 플래그를 매 번 태스크 스위칭이 발생할 때 마다 새로 지정해 주고, 해당 명령어를 실행함으로써 테스트를 합니다.


- 만약 TS 플래그와 EM 플래그가 1로 세팅되는 경우, X87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어를 실행하면 PAUSE, PREFETCHh, SFENCE, LFENCE, MFENCE, MOVNTI, CLFLUSH, CRC32, POPCNT 예외와 함께 Device-not-available 예외(#NM)가 발생합니다. 

- 만약 TS 플래그와 MP 플래그가 1로 세팅되고 EM 플래그가 0으로 세팅되는 경우 x87 FPU WAIT/FWAIT 명령어 실행 시 #NM 예외가 우선적으로 발생하지 않습니다.

- 만약 EM 플래그가 1로 세팅되는 경우 TS 플래그는 X87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어 실행에 대해 영향을 주지 않습니다.


프로세서는 태스크 스위치 과정에서 X87 FPU, XMM, MXCSR 레지스터에 대한 컨텍스트를 자동으로 저장하지 않습니다. 대신, TS 플래그를 세팅하고 새로운 태스크에 대한 명령어들 중  x87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어가 등장할 때 마다 #NM 예외를 발생시킵니다. #NM 예외에 대한 Fault Handler는 TS 플래그를 CLTS 명령어를 이용하여 0으로 만들고 사용이 가능하고, X87 FPU, XMM, MXCSR 레지스터에 대한 컨텍스트를 저장합니다. 만약 태스크가 x87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어를 만나지 않는다면 해당 컨텍스트는 저장되지 않습니다.


아래의 표는 프로세서가 TS, EM, MP 플래그가 1로 세팅된 상태로 x87 FPU/MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어를 만났을 때 어떠한 동작을 하는 지에 대해 나타낸 것입니다.



EM (Emulation, bit 2)

EM 플래그가 1로 세팅되면 내부 혹은 외부의 X87 FPU가 없다는 것을 의미합니다. 0으로 세팅되면 존재한다는 것을 의미합니다. 해당 플래그는 MMX/SSE/SSE2/SSE3/SSSE3/SSE4 명령어의 실행에도 영향을 미칩니다.


EM 플래그가 1로 세팅되면, X87 FPU 명령어가 실행될 시 device-not-available 예외(#NM)가 발생합니다. 이 플래그는 프로세서가 X87 FPU를 가지고 있지 않고, 외부 FPU와 연결되지 않은 경우에만 1로 세팅되어야 합니다. 해당 플래그는 모든 부동소수점 연산 관련 연산자가 소프트웨어 에뮬레이션으로 처리되도록 강제합니다. 


또한 EM 플래그가 1로 세팅되면, MMX 명령어의 실행이 invalid-opcode 예외(#UD)를 발생시킵니다. 따라서 만약 IA-32 혹은 인텔 64비트 프로세서가 MMX 기술을 통합하여 사용하고 있다면, 해당 플래그는 반드시 0으로 세팅되도록 하여 MMX 명령어의 실행이 이루어질 수 있도록 해야 합니다.


SSE/SSE2/SSE3/SSSE3/SSE4 확장에 대해서도 비슷하게 작용합니다. EM 플래그가 1로 세팅되면 SSE/SSE2/SSE3/SSSE3/SSE4 명령어의 실행은 invalid-opcode 예외(#UD)를 발생시킵니다. 만약 IA-32 프로세서 혹은 인텔 64비트 프로세서가 SSE/SSE2/SSE3/SSSE3/SSE4 확장을 통합하여 사용하고 있다면, 역시 해당 플래그는 0으로 세팅되어야 합니다. SSE/SSE2/SSE3/SSSE3/SSE4 명령어들 중 EM flag의 영향을 받지 않는 명령어는 다음과 같습니다 : PAUSE, PREFETCHh, SFENCE, LFENCE, MFENCE, MOVNTI, CLFLUSH, CRC32, POPCNT


MP (Monitor Coprocessor, bit 1)

TS 플래그와 함께 WAIT 명령어에 대한 상호작용을 컨트롤합니다. TS 플래그와 MP 플래그가 1로 세팅되어 있으면 WAIT 명령어는 device-not-available 예외(#NM)를 발생시킵니다. MP 플래그가 0인 경우 WAIT 명령어는 TS 플래그의 세팅을 무시하고 작동합니다. 


PE (Protection Enable, bit 0)

PE 플래그가 1로 세팅되어 있으면 보호 모드(32비트 모드)를 활성화합니다. 0으로 세팅되는 경우 리얼 모드(16비트 모드)로 작동합니다. 해당 플래그는 페이징을 직접적으로 활성화할 수 없습니다. 이는 세그먼트 레벨의 보호 모드만 활성화하는 플래그입니다. 페이징을 활성화하고자 하면 PE 플래그와 PG 플래그를 모두 세팅해야 합니다.


해당 플래그들이 어떠한 역할을 하는지에 대해 자세하게 알아보았습니다. 이제 각 필드들을 어떻게 설정할지에 대해 생각해 봅시다.


MINT64에서는 32비트 모드는 단지 전환을 위한 모드이기 때문에 세그먼테이션 기능만을 사용합니다. 따라서 페이징, 캐시, 메모리 정렬, 메모리 쓰기 금지 기능은 사용하지 않습니다. FPU 역시 사용하지 않습니다. FPU 관련 설정은 조금 자세하게 보겠습니다. 일단 intel i5-6200u processor는 내장 FPU가 있기 때문에 EM 필드는 0으로 설정합니다. 또한, ET 필드는 1로 설정합니다. 임시로 초기화한 상태이기 때문에, MP 필드와 NE 필드, TS 필드를 1로 설정하여 FPU 연산자가 실행되면 예외가 발생하도록 합니다. 


정리해 보면, PG = 0, CD = 1, NW = 0, AM = 0, WP = 0, NE = 1, ET = 1, TS = 1, EM = 0, MP = 1, PE = 1로 세팅됩니다.


CR0 레지스터는 32비트 크기이기 때문에 범용 레지스터 중 32비트를 나타내는 EAX를 사용합니다. 플래그를 16진수로 변환하면 0x4000003B가 됩니다.


이제 CR0 레지스터를 설정하는 코드를 작성해 봅시다.


mov eax, 0x4000003B

mov cr0, eax


보호 모드 전환을 위한 단계는 거의 마무리되었습니다. 이제 CS 세그먼트 셀렉터를 교체하여 주면 됩니다. 해당 작업을 위해서는 jmp 명령과 세그먼트 레지스터 접두사를 사용하여야 합니다. 세그먼트 셀렉터에 어드레스를 설정하여 GDT 내의 디스크립터에 지시를 내릴 것입니다. 단, 보호 모드는 기존 리얼 모드와 다르게 세그먼트 base address를 사용하지 않고, 디스크립터의 address(GDT에서의 오프셋)를 사용합니다.


다음 코드는 위에서 언급한, 커널 코드 세그먼트를 사용하여 보호 모드로 전환하고 나머지 세그먼트 셀렉터를 커널 데이터 세그먼트 디스크립터로 초기화하는 코드입니다.


jmp dword 0x08: (PROTECTMODE - $$ + 0x10000)


[BITS 32]

PROTECTMODE:

mov ax, 0x10

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax


mov ss, ax

mov esp, 0xFFFE

mov ebp, 0xFFFE


이제 리얼 모드에서 보호 모드로 전환하는 과정을 마무리하였습니다. 다음 글에서는 16비트 코드를 32비트 코드로 변환하고, 커널 이미지를 빌드하며, 가상 OS 이미지를 교체하여 커널 코딩의 직전 단계까지 진행할 것입니다.




Comments