반응형

→ 왜 GDT 테이블을 교환해야 하는가

 

지금까지 사용한 코드와 데이터 세그먼트 디스크립터는 보호 모드 커널 엔트리 포인트 영역에서 어셈블리어로 작성한 것입니다.

 

이 영역에 TSS 세그먼트TSS 세그먼트 디스크립터를 추가해도 되지만, 커널 엔트리 포인터 영역(512byte)에 비해 104byte의 TSS 세그먼트 디스크립터가 너무 큰 게 문제입니다.

 

그리고 멀티코어를 활성화하게 되면 코어 별로 TSS 세그먼트가 필요하므로 미리 대비하여 1MB 이상의 공간에 GDT 테이블을 생성하는 것입니다.

 

C 코드를 사용하여 구조체와 매크로 그리고 함수만 잘 정의하면 코드, 데이터 디스크립터, TSS 디스크립터까지 전부 처리할 수 있습니다.

 


 

→ GDT 테이블 생성과 TSS 세그먼트 디스크립터 추가

 

GDTGDT 테이블과 GDT에 대한 정보를 나타내는 자료구조로 구성되어 있기 때문에 이 두 가지 자료구조를 정의하고 각 자료구조에 맞는 매크로도 함께 정의합니다.

 

GDT 테이블과 GDT 정보를 나타내는 자료구조는 이전에 이미 어셈블리어로 작성해 봤으므로 바로 코드를 작성합니다.

 

아래는 코드와 데이터 세그먼트 디스크립터 그리고 TSS 세그먼트 디스크립터와 TSS 세그먼트로 사용할 자료구조와 매크로입니다.

 

GDTENTRY8, GETENTRY16 자료구조의 필드이전의 IA-32e 모드의 TSS 디스크립터 그림을 그대로 옮긴 것이고, TSSSEEGMENT 자료구조는 IA-32e 모드의 TSS 세그먼트 구조의 내용을 참고하면 됩니다.

 

세그먼트 디스크립터, GDT 테이블, TSS 세그먼트를 위한 자료구조와 매크로 1

 

세그먼트 디스크립터, GDT 테이블, TSS 세그먼트를 위한 자료구조와 매크로 2

 

세그먼트 디스크립터, GDT 테이블, TSS 세그먼트를 위한 자료구조와 매크로 3

 

 

자료구조매크로를 정의했으니 이제 디스크립터를 생성하는 함수를 작성합니다.

 

디스크립터를 생성하는 함수파라미터로 넘어온 각 필드의 값을 세그먼트 디스크립터의 구조에 맞추어 삽입해주는 역할을 합니다.

 

세그먼트 디스크립터의 각 필드를 조합하는 방법은 이미 이전에 설명했으므로 바로 코드로 넘어갑니다.

 

아래는 GDTENTRY8, GDTENTRY16의 자료구조에 맞추어 값을 삽입하는 kSetGDTEntry8(), kSetGDTEntry16() 함수와 이를 사용하여 GDT를 생성하는 코드입니다.

 

널(NULL) 디스크립터와 커널 코드, 커널 데이터, TSS 세그먼트 디스크립터 순으로 생성합니다.

 

세그먼트 디스크립터를 생성하는 함수 코드와 사용 예 1

 

세그먼트 디스크립터를 생성하는 함수 코드와 사용 예 2

 

위 코드에서 GDTR 자료구조의 시작 주소를 0x142000으로 설정하고, 그 이후 주소부터 각 자료구조를 위치시킨 이유는 0x100000(1MB) 영역부터 264KB를 페이지 테이블로 사용하기 때문입니다.

 

그러므로 그 이후부터 GDT와 TSS 세그먼트를 생성하여 IA-32e 모드에서 사용하게 합니다.

 

1MB ~ 2MB 영역의 메모리 구조

 


 

→ TSS 세그먼트 초기화

 

TSS 세그먼트이전의 IA-32e 모드의 TSS 세그먼트 구조 그림과 같이 스택에 대한 필드와 I/O 맵에 대한 필드로 구성되어 있습니다.

 

FS64 OS I/O 맵을 사용하지 않으며 스택 스위칭 방식 역시 고전적인 방식이 아닌 IST 방식을 사용하므로 이에 맞추어 TSS 세그먼트 필드를 설정합니다.

 

 

I/O 맵을 사용하지 않게 설정하는 방법I/O 맵 기준 주소를 TSS 세그먼트 디스크립터에서 설정한 Limit 필드 값보다 크게 설정하면 됩니다.

 

TSS 세그먼트 디스크립터에서는 TSSSEGMENT 자료구조의 크기만큼 Limit를 설정했으므로 그보다 큰 0xFFFF를 설정합니다.

 

IST 역시 IDT 게이트 디스크립터의 IST 필드를 0이 아닌 값으로 설정하고, TSS 세그먼트의 해당 IST 영역에 핸들러가 사용할 스택 주소를 설정하면 되는데, 단 IST의 스택 주소는 반드시 16byte로 정렬된 위치에서 시작해야 한다는 점만 주의하면 됩니다.

 

TSS 세그먼트를 설정하는 함수를 작성합니다.

 

아래는 I/O 맵을 사용하지 않고, 7 ~ 8MB 영역을 IST 1로 사용하도록 TSS 세그먼트를 초기화하는 코드입니다.

 

TSS 세그먼트를 초기화하는 코드

 


 

→ GDT 테이블 교체와 TSS 세그먼트 로드

 

GDT 테이블TSS 세그먼트 생성을 완료했으니 이제 GDT 테이블을 교체하고 TSS 세그먼트를 프로세서에 로드합니다.

 

GDT 테이블을 교체하는 방법은 예전에 살펴본 것과 같이 LGDT 명령을 사용하여 GDT 정보를 수정하면 됩니다.

세그먼트 디스크립터의 오프셋은 기존 테이블과 같으므로 변경할 필요가 없습니다.

 

 

GDT는 LGDT 명령으로 갱신할 수 있습니다.

 

 

TSS 세그먼트를 로드하려면 x86 프로세서에는 태스크에 관련된 정보를 저장하는 태스크 레지스터(Task Register, TR)가 있고, TR 레지스터는 현재 프로세서가 수행 중인 태스크의 정보를 관리하며, GDT 테이블 내에 TSS 세그먼트 디스크립터의 오프셋이 저장되어 있기 때문에 LTR 명령어를 사용하여 GDT 테이블 내의 TSS 세그먼트 인덱스인 0x18을 지정함으로써 TSS 세그먼트를 프로세서에 설정합니다.

 

GDT 테이블을 교체하는 방법TSS 세그먼트를 설정하는 방법을 알았으니, 실제 코드로 작성합니다.

 

LGDT, LTR 명령어는 모두 어셈블리어 명령이므로, 이를 수행하는 어셈블리어 함수를 만들어 C 코드에서 호출합니다.

 

아래는 LGDT, LTR 명령어를 수행하는 어셈블리어 함수와 그 함수를 선언한 코드입니다.

 

LGDT와 LTR 명령어를 수행하는 어셈블리어 함수

 

C 함수 선언 코드

 

이렇게 GDT 테이블 교체와 TSS 세그먼트 생성에 대한 준비가 끝났습니다.

 

작성한 함수들을 호출하여 프로세서에 GDT 테이블을 갱신하고 TSS 세그먼트를 프로세서에 로드합니다.

 

아래는 작성한 함수를 호출하여 작업을 마무리 하는 코드입니다

 

GDT 테이블을 갱신하고 TSS 세그먼트를 프로세서에 로드하는 코드

 


 

→ IDT 테이블 생성

 

IDT 테이블은 IDT 게이트 디스크립터로 구성됩니다.

 

IDT 게이트 디스크립터세그먼트 디스크립터와 구조적으로 다르지만, 타입 필드와 DPL, 그리고 P 필드의 위치가 같고 세그먼트 디스크립터보다 필드가 간단하므로 더 쉽게 처리할 수 있습니다.

 

아래는 IDT 게이트 디스크립터의 자료구조와 매크로 입니다.

 

IDT 테이블에 대한 정보를 나타내는 자료구조는 GDT와 큰 차이가 없습니다.

 

IDT 게이트 디스크립터를 위한 자료구조와 매크로

 

IDT 게이트 디스크립터를 설정하는 함수 역시 kSetGDTEntry16() 함수와 마찬가지로 16byte로 구성된 자료구조에 값을 설정합니다.

 

아래는 IDT 게이트 디스크립터에 데이터를 설정하는 함수입니다.

 

IDT 게이트 디스크립터를 생성하는 코드

 

kSetIDTEntry() 함수는 파라미터로 넘겨진 값을 자료구조로 채워주는 역할만 합니다.

 

실제로 IDT 게이트 디스크립터를 생성하는 핵심은 파라미터를 넘겨주는 부분이기 때문에 각 파라미터의 의미와 파라미터를 설정하는 방법에 대해 알아봅니다.

 

 

pstEntry 파라미터값을 저장할 IDT 게이트 디스크립터의 주소를 넘겨주는 용도로 사용합니다.

 

pvHandler 파라미터해당 인터럽트 또는 예외가 발생했을 때 실행할 핸들러 함수의 주소입니다.

 

이번에는 인터럽트나 예외가 발생했을 때 핸들러가 정상적으로 실행되는지 확인하는 것이 목적이므로 임시 핸들러 함수를 작성하여 주소를 넘겨줍니다.

 

wSelector 파라미터인터럽트나 예외가 발생했을 때 교체할 CS 세그먼트 셀렉터의 값입니다.

일반적으로 핸들러 함수는 커널 모드에서 동작하므로 세그먼트 셀렉터 역시 커널 코드 세그먼트인 0x08을 설정합니다.

 

bIST 파라미터인터럽트나 예외가 발생했을 때 IST 중 어느 것을 사용할지를 설정하는 용도로 사용합니다.

FS64 OS에서는 1번 IST만 사용하므로 1로 설정합니다.

 

bFlags와 bType권한(특권 레벨)과 게이트 타입을 설정하는 파라미터입니다.

 

FS64 OS의 모든 핸들러커널 레벨에서 동작하며 핸들러 수행 중에 인터럽트가 발생하는 것을 허락하지 않으므로 IDT_FLAGS_KERNEL 매크로와 IDT_TYPE_INTERRUPT 매크로를 각각 설정합니다.

 

아래는 앞에서 설명한 내용을 바탕으로 IDT 테이블을 생성하는 코드입니다.

 

IDTR 자료구조의 시작 주소는 앞서 생성한 TSS 세그먼트 이후에 위치하게 0x1420A0으로 설정했으며, 인터럽트와 예외는 최대 100개까지 처리 가능하게 했습니다.

 

그리고 임시 인터럽트 핸들러인 kDummyHandler() 함수는 테스트를 위해 화면에 메시지를 출력하고 정지하게 해서 인터럽트 또는 예외 발생 시 쉽게 확인할 수 있도록 합니다.

 

IDT 테이블을 생성하는 코드와 임시 핸들러 코드

 

이렇게 IDT 테이블 준비가 끝났습니다.

 

생성된 IDT 테이블을 프로세서에 로드시켜 인터럽트나 예외가 발생했을 때 참조하도록 합니다.

 


 

→ IDT 테이블 로드

 

IDT 테이블을 프로세서에 로드하는 방법은 GDT 테이블을 로드하는 방법과 비슷합니다.

 

x86 프로세서에는 IDT 테이블에 대한 정보를 저장하는 IDTR 레지스터가 있으며, LIDT 명령어를 사용하여 IDT 테이블에 대한 정보를 갖고 있는 자료구조의 주소를 넘겨줌으로써 프로세서에 로드할 수 있습니다.

 

아래는 LIDT 명령어를 수행하는 어셈블리어 함수와 그 함수를 선언한 코드입니다.

 

LIDT 명령어를 수행하는 어셈블리어 함수와 C 함수 선언 코드

 

이제 IDT 테이블을 프로세서에 설정하기 위한 작업이 모두 끝났습니다.

 

이제 남은 것은 kInitializeIDTTables()kLoadIDTR() 함수를 호출하여 프로세서에 로드하는 일이 남았습니다.

간단히 바로 코드를 봅니다.

 

함수를 통해 IDT 테이블을 생성하고 로드하는 코드

 

반응형

+ Recent posts