본문 바로가기

MINT64 OS 개발

12장. GDT, IDT 테이블, TSS 세그먼트를 추가해 인터럽트에 대비하자

Ⅰ. GDT 테이블 교환과 TSS 세그먼트 디스크립터 추가

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

  • 현재까지 사용한 코드&데이터 세그먼트 디스크립터는 보호 모드 커널 엔트리 포인트영역에서 어셈블리어로 작성한 것이다.
    • 멀티 코어를 활성화하게 되면, 코어 별로 TSS 세그먼트가 필요하므로, 이를 대비하여 1MB 이상의 공간에 GDT 테이블을 생성한다.
  • C코드로 GDT 테이블을 새로 작성한다.
  • 세그먼트 디스크립터 & GDT 테이블 & TSS 세그먼트를 위한 자료구조와 매크로
// 조합에 사용할 기본 매크로
#define GDT_TYPE_CODE           0x0A
#define GDT_TYPE_DATA           0x02
#define GDT_TYPE_TSS            0x09
#define GDT_FLAGS_LOWER_S       0x10
#define GDT_FLAGS_LOWER_DPL0    0x00
#define GDT_FLAGS_LOWER_DPL1    0x20
#define GDT_FLAGS_LOWER_DPL2    0x40
#define GDT_FLAGS_LOWER_DPL3    0x60
#define GDT_FLAGS_LOWER_P       0x80
#define GDT_FLAGS_UPPER_L       0x20
#define GDT_FLAGS_UPPER_DB      0x40
#define GDT_FLAGS_UPPER_G       0x80

// 실제로 사용할 매크로
// Lower Flags는 Code/Data/TSS, DPL0, Present로 설정
#define GDT_FLAGS_LOWER_KERNELCODE ( GDT_TYPE_CODE | GDT_FLAGS_LOWER_S | \
        GDT_FLAGS_LOWER_DPL0 | GDT_FLAGS_LOWER_P )
#define GDT_FLAGS_LOWER_KERNELDATA ( GDT_TYPE_DATA | GDT_FLAGS_LOWER_S | \
        GDT_FLAGS_LOWER_DPL0 | GDT_FLAGS_LOWER_P )
#define GDT_FLAGS_LOWER_TSS ( GDT_FLAGS_LOWER_DPL0 | GDT_FLAGS_LOWER_P )
#define GDT_FLAGS_LOWER_USERCODE ( GDT_TYPE_CODE | GDT_FLAGS_LOWER_S | \
        GDT_FLAGS_LOWER_DPL3 | GDT_FLAGS_LOWER_P )
#define GDT_FLAGS_LOWER_USERDATA ( GDT_TYPE_DATA | GDT_FLAGS_LOWER_S | \
        GDT_FLAGS_LOWER_DPL3 | GDT_FLAGS_LOWER_P )

// Upper Flags는 Granulaty로 설정하고 코드 및 데이터는 64비트 추가
#define GDT_FLAGS_UPPER_CODE ( GDT_FLAGS_UPPER_G | GDT_FLAGS_UPPER_L )
#define GDT_FLAGS_UPPER_DATA ( GDT_FLAGS_UPPER_G | GDT_FLAGS_UPPER_L )
#define GDT_FLAGS_UPPER_TSS ( GDT_FLAGS_UPPER_G )

// 1바이트로 정렬
#pragma pack( push, 1 )

// GDTR 및 IDTR 구조체
typedef struct kGDTRStruct
{
    WORD wLimit;
    QWORD qwBaseAddress;
    // 16바이트 어드레스 정렬을 위해 추가
    WORD wPading;
    DWORD dwPading;
} GDTR, IDTR;

// 8바이트 크기의 GDT 엔트리 구조
typedef struct kGDTEntry8Struct
{
    WORD wLowerLimit;
    WORD wLowerBaseAddress;
    BYTE bUpperBaseAddress1;
    // 4비트 Type, 1비트 S, 2비트 DPL, 1비트 P
    BYTE bTypeAndLowerFlag;
    // 4비트 Segment Limit, 1비트 AVL, L, D/B, G
    BYTE bUpperLimitAndUpperFlag;
    BYTE bUpperBaseAddress2;
} GDTENTRY8;

// 16바이트 크기의 GDT 엔트리 구조
typedef struct kGDTEntry16Struct
{
    WORD wLowerLimit;
    WORD wLowerBaseAddress;
    BYTE bMiddleBaseAddress1;
    // 4비트 Type, 1비트 0, 2비트 DPL, 1비트 P
    BYTE bTypeAndLowerFlag;
    // 4비트 Segment Limit, 1비트 AVL, 0, 0, G
    BYTE bUpperLimitAndUpperFlag;
    BYTE bMiddleBaseAddress2;
    DWORD dwUpperBaseAddress;
    DWORD dwReserved;
} GDTENTRY16;

// TSS Data 구조체
typedef struct kTSSDataStruct
{
    DWORD dwReserved1;
    QWORD qwRsp[ 3 ];
    QWORD qwReserved2;
    QWORD qwIST[ 7 ];
    QWORD qwReserved3;
    WORD wReserved;
    WORD wIOMapBaseAddress;
} TSSSEGMENT;

#pragma pack ( pop )
  • 세그먼트 디스크립터를 생성하는 함수 코드와 사용 예
    • 디스크립터를 생성하는 함수는 파라미터로 넘어온 각 필드의 값을 세그먼트 디스크립터의 구조에 맞추어 삽입해주는 역할을 수행한다.
    • GDTR 자료구조의 시작 어드레스를 0x142000으로 설정하고, 그 이후 어드레스부터 각 자료구조를 위치시킨 이유
      • 0x100000(1MB) 영역부터 264KB를 페이지 테이블로 사용하고 있기 때문이다.
      //  GDT 테이블을 초기화
      
      void kInitializeGDTTableAndTSS( void )
      {
          GDTR* pstGDTR;
          GDTENTRY8* pstEntry;
          TSSSEGMENT* pstTSS;
          int i;
          
          // GDTR 설정
          pstGDTR = ( GDTR* ) 0x142000;
          pstEntry = ( GDTENTRY8* ) ( 0x142000 + sizeof( GDTR ) );
          pstGDTR->wLimit = ( sizeof( GDTENTRY8 ) * 3 ) + ( sizeof( GDTENTRY16 ) * 1 ) - 1;
          pstGDTR->qwBaseAddress = ( QWORD ) pstEntry;
          // TSS 영역 설정
          pstTSS = ( TSSSEGMENT* ) ( ( QWORD ) pstEntry + GDT_TABLESIZE );
      
          // NULL, 64비트 Code/Data, TSS를 위해 총 4개의 세그먼트를 생성한다.
          kSetGDTEntry8( &( pstEntry[ 0 ] ), 0, 0, 0, 0, 0 );
          kSetGDTEntry8( &( pstEntry[ 1 ] ), 0, 0xFFFFF, GDT_FLAGS_UPPER_CODE, 
                  GDT_FLAGS_LOWER_KERNELCODE, GDT_TYPE_CODE );
          kSetGDTEntry8( &( pstEntry[ 2 ] ), 0, 0xFFFFF, GDT_FLAGS_UPPER_DATA,
                  GDT_FLAGS_LOWER_KERNELDATA, GDT_TYPE_DATA );
          kSetGDTEntry16( ( GDTENTRY16* ) &( pstEntry[ 3 ] ), ( QWORD ) pstTSS, 
                  sizeof( TSSSEGMENT ) - 1, GDT_FLAGS_UPPER_TSS, GDT_FLAGS_LOWER_TSS, 
                  GDT_TYPE_TSS ); 
          
          // TSS 초기화 GDT 이하 영역을 사용함
          kInitializeTSSSegment( pstTSS );
      }
      
      //  8바이트 크기의 GDT 엔트리에 값을 설정
      //  코드와 데이터 세그먼트 디스크립터
      void kSetGDTEntry8( GDTENTRY8* pstEntry, DWORD dwBaseAddress, DWORD dwLimit,
              BYTE bUpperFlags, BYTE bLowerFlags, BYTE bType )
      {
          pstEntry->wLowerLimit = dwLimit & 0xFFFF;
          pstEntry->wLowerBaseAddress = dwBaseAddress & 0xFFFF;
          pstEntry->bUpperBaseAddress1 = ( dwBaseAddress >> 16 ) & 0xFF;
          pstEntry->bTypeAndLowerFlag = bLowerFlags | bType;
          pstEntry->bUpperLimitAndUpperFlag = ( ( dwLimit >> 16 ) & 0xFF ) | 
              bUpperFlags;
          pstEntry->bUpperBaseAddress2 = ( dwBaseAddress >> 24 ) & 0xFF;
      }
      
      //  16바이트 크기의 GDT 엔트리에 값을 설정
      //  TSS 세그먼트 디스크립터를 설정하는데 사용
      void kSetGDTEntry16( GDTENTRY16* pstEntry, QWORD qwBaseAddress, DWORD dwLimit,
              BYTE bUpperFlags, BYTE bLowerFlags, BYTE bType )
      {
          pstEntry->wLowerLimit = dwLimit & 0xFFFF;
          pstEntry->wLowerBaseAddress = qwBaseAddress & 0xFFFF;
          pstEntry->bMiddleBaseAddress1 = ( qwBaseAddress >> 16 ) & 0xFF;
          pstEntry->bTypeAndLowerFlag = bLowerFlags | bType;
          pstEntry->bUpperLimitAndUpperFlag = ( ( dwLimit >> 16 ) & 0xFF ) | bUpperFlags;
          pstEntry->bMiddleBaseAddress2 = ( qwBaseAddress >> 24 ) & 0xFF;
          pstEntry->dwUpperBaseAddress = qwBaseAddress >> 32;
          pstEntry->dwReserved = 0;
      }
      

 

2. TSS 세그먼트 초기화

  • TSS 세그먼트는 스택에 대한 필드와 I/O 맵에 대한 필드로 구성되어 있다.
  • mint64 OS는 I/O 맵을 사용하지 않으며, IST방식을 사용하므로 이에 맞춰 TSS 세그먼트 필드를 설정한다.
  • I/O 맵을 사용하지 않게 설정하는 방법
    • I/O 맵 기준 주소를 TSS 세그먼트 디스크립터에서 설정한 Limit필드 값보다 크게 설정하면 된다.
    • TSS 세그먼트 디스크립터에서는 TSSSEGMENT 자료구조의 크기만큼 Limit를 설정했으므로 그보다 큰 0xFFFF를 설정한다.
  • IDT 게이트 디스크립터의 IST 필드를 0이 아닌 값으로 설정하고, TSS 세그먼트의 해당 IST영역에 핸들러가 사용할 스택 어드레스를 설정한다.
  • (단, IST의 스택 어드레스는 반드시 16byte로 정렬된 위치에서 시작해야 하는 것을 알아야 한다.)
  • I/O 맵을 사용하지 않고 7 ~ 8MB 영역을 IST 1로 사용하도록, TSS 세그먼트를 초기화하는 코드이다.
void kInitializeTSSSegment( TSSSEGMENT* pstTSS ){
		kMemSet( pstTSS, 0, sizeof( TSSDATA ) );
		pstTSS -> qwIST[ 0 ] = 0x800000;
		pstTSS -> wIOMapBaseAddress = 0xFFFF;
}

 

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

  • GDT 테이블 교체 방법은 LGDT 명령을 사용하여 GDT 정보를 수정하면 된다.
  • 세그먼트 디스크립터의 오프셋은 기존 테이블과 같으므로 변경할 필요가 없다.
  • LTR 명령어를 사용하여 GDT 테이블 내의 TSS 세그먼트 인덱스인 0x18을 지정함으로써 TSS세그먼트를 프로세서에 설정할 수 있다.
  • LGDT와 LTR 명령어를 수행하는 어셈블리어 함수 
    ; GDTR 레지스터에 GDT 테이블을 설정
    ;    PARAM: GDT 테이블의 정보를 저장하는 자료구조의 어드레스
    kLoadGDTR:
    		 lgdt  [ rdi ]     ; 파라미터 1(GDTR의 어드레스)를 프로세서에 로드하여
                           ; GDT 테이블을 설정
         ret
    
    ; TR 레지스터에 TSS 세그먼트 디스크립터 설정
    ;    PARAM: TSS 세그먼트 디스크립터의 오프셋
    kLoadTR:
         ltr di            ; 파라미터 1(TSS 세그먼트 디스크립터의 오프셋)을 프로세서에
                           ; 설정하여 TSS 세그먼트를 로드
         ret
  • C함수 선언
void kLoadGDTR( QWORD qwGDTRAddress );
void kLoadTR ( WORD wTSSSegmentOffset );
  • DT 테이블을 갱신하고, TSS 세그먼트를 프로세서에 로드하는 코드
void Main ( void )
{
    ``` 생략 ```

		kInitializeGDTTableAndTSS( );
		kLoadGDTR( 0x142000 );
    kLoadTR ( 0x18 );

		``` 생략 ```
}

Ⅱ. IDT 테이블 생성, 인터럽트, 예외 핸들러 등록

1. IDT 테이블 생성

  • IDT 게이트 디스크립터로 구성된다.
// 조합에 사용할 기본 매크로
#define IDT_TYPE_INTERRUPT      0x0E
#define IDT_TYPE_TRAP           0x0F
#define IDT_FLAGS_DPL0          0x00
#define IDT_FLAGS_DPL1          0x20
#define IDT_FLAGS_DPL2          0x40
#define IDT_FLAGS_DPL3          0x60
#define IDT_FLAGS_P             0x80
#define IDT_FLAGS_IST0          0
#define IDT_FLAGS_IST1          1

// 실제로 사용할 매크로
#define IDT_FLAGS_KERNEL        ( IDT_FLAGS_DPL0 | IDT_FLAGS_P )
#define IDT_FLAGS_USER          ( IDT_FLAGS_DPL3 | IDT_FLAGS_P )

// 1바이트로 정렬
#pragma pack( push, 1 )

// IDT 게이트 디스크립터 구조체
typedef struct kIDTEntryStruct
{
    WORD wLowerBaseAddress;
    WORD wSegmentSelector;
    // 3비트 IST, 5비트 0
    BYTE bIST;
    // 4비트 Type, 1비트 0, 2비트 DPL, 1비트 P
    BYTE bTypeAndFlags;
    WORD wMiddleBaseAddress;
    DWORD dwUpperBaseAddress;
    DWORD dwReserved;
} IDTENTRY;

#pragma pack ( pop )
  • IDT 게이트 디스크립터를 생성하는 코드
    • kSetGDTEntry16( ) 함수와 마찬가지로, 16byte로 구성된 자료구조에 값을 설정한다.
    • kSetIDTEntry( ) 함수는 파라미터로 넘겨진 값을 자료구조로 채워주는 역할만 한다.
    void kSetIDTEntry( IDTENTRY* pstEntry, void* pvHandler, WORD wSelector, 
            BYTE bIST, BYTE bFlags, BYTE bType )
    {
        pstEntry->wLowerBaseAddress = ( QWORD ) pvHandler & 0xFFFF;
        pstEntry->wSegmentSelector = wSelector;
        pstEntry->bIST = bIST & 0x3;
        pstEntry->bTypeAndFlags = bType | bFlags;
        pstEntry->wMiddleBaseAddress = ( ( QWORD ) pvHandler >> 16 ) & 0xFFFF;
        pstEntry->dwUpperBaseAddress = ( QWORD ) pvHandler >> 32;
        pstEntry->dwReserved = 0;
    }
    
  • IDT 테이블을 생성하는 코드와 임시 핸들러 코드
    • kDummyHandler( ) 함수는 테스트를 위해, 화면에 메시지를 출력하고 정지하게 해서 인터럽트 또는 예외 발생 시 쉽게 확인할 수 있도록 하였다.
//  IDT 테이블을 초기화
void kInitializeIDTTables( void )
{
    IDTR* pstIDTR;
    IDTENTRY* pstEntry;
    int i;
        
    // IDTR의 시작 어드레스
    pstIDTR = ( IDTR* ) 0x1420A0;
    // IDT 테이블의 정보 생성
    pstEntry = ( IDTENTRY* ) ( 0x1420A0 + sizeof( IDTR ) );
    pstIDTR->qwBaseAddress = ( QWORD ) pstEntry;
    pstIDTR->wLimit = 100 * sizeof( IDTENTRY ) - 1;
    
    // 0~99까지 벡터를 모두 DummyHandler로 연결
    for( i = 0 ; i < 100 ; i++ )
    {
        kSetIDTEntry( &( pstEntry[ i ] ), kDummyHandler, 0x08, IDT_FLAGS_IST1, 
            IDT_FLAGS_KERNEL, IDT_TYPE_INTERRUPT );
    }
}

//  임시 예외 또는 인터럽트 핸들러
void kDummyHandler( void )
{
    kPrintString( 0, 0, "====================================================" );
    kPrintString( 0, 1, "          Dummy Interrupt Handler Execute~!!!       " );
    kPrintString( 0, 2, "           Interrupt or Exception Occur~!!!!        " );
    kPrintString( 0, 3, "====================================================" );

    while( 1 ) ;
}

 

2. IDT 테이블 로드

  • LIDT 명령어를 수행하는 어셈블리어 함수와 C 함수 선언 코드
; IDTR 레지스터에 IDT 테이블을 설정
;    PARAM: IDT 테이블의 정보를 저장하는 자료구조의 어드레스
kLoadIDTR:
		 lidt [ rdi ]   ; 파라미터 1(IDTR의 어드레스)를 프로세서에 로드하여, IDT 테이블을 설정
     ret

// C 함수 선언
void kLoadIDTR( QWORD qwIDTRAddress );
  • 함수를 통해, IDT 테이블을 생성하고 로드하는 코드
void Main ( void )
{
	``` 생략 ```
    kInitializeIDTTables( );
    kLoadIDTR( 0x1420A0 );
    
    ``` 생략 ```
}

Ⅲ. IDT, TSS 통합과 빌드

키보드로 0을 누를 경우, 출력되는 모습