[OS 개발 9] 32비트 커널 로더(3) - GDT의 개념과 적용


1. GDT의 개념

이전 포스팅을 마치면서 잠깐 GDT에 대해 언급하였습니다. 16비트 리얼 모드에서 32비트 보호 모드로 운영 모드를 바꿔야 하며, 이를 위해 GDT를 사용한다고 했는데요,. 본 포스트에서는 GDT 에 대해 알아보도록 하겠습니다.

 

GDT를 풀어쓰자면 글로벌 디스크립터 테이블(Global Descriptor Table, GDT)이며, 말 그대로 무언가에 대한 기술서를 테이블 형식으로 한데 모아둔 형태를 의미합니다.

 

여기에서 무언가는 각 세그먼트 영역을 나타냅니다. 즉, 세그먼트 영역에 대한 데이터를 일정한 디스크립터 형식으로 기술하고 이를 하나의 테이블에 모아두고자 하는 것이 GDT를 사용하는 목적이죠.

 

그렇다면 이 디스크립터라는 녀석은 도대체 어떤 형식을 갖추고 있는 걸까요? 다음 그림을 참고하시기 바랍니다.

 

GDT 디스크립터 구조GDT 디스크립터 구조

 

이것이 하나의 64비트 디스크립터입니다.

이와 같은 디스크립터들을 아래와 같이 모아놓은 것이 글로벌 디스크립터 테이블, 즉, GDT인 것입니다. 

 

Global Descriptor Table (GDT)Global Descriptor Table (GDT)

 

각 디스크립터의 필드에 대한 설명은 다음과 같습니다.

 

 디스크립터 필드

설 명 

 Segment Limit

 1.  세그먼트의 한계점(크기)를 나타내며, 오프셋은 이 숫자를 넘길 수 없다.

 2. G비트에 저장된 값에 따라 한계점이 달라진다.

     예) G가 0이고 Limit 값이 0x1234일 경우: 세그먼트의 크기는 그대로 0x1234

           G가 1이고 Limit 값이 0x1234일 경우: 4KB * 0x1234 = 0xFFF * 0x1234 = 0x1234FFF (세그먼트 * 4KB)

 Base Address

  세그먼트의 물리주소이며, 상위와 하위 주소로 나누어 저장한다.

 속성

P

 1. 세그먼트의 메모리 존재 여부를 나타냄

 2. 페이징 기능과 관련되어있다. 

DPL

 1. 커널레벨인지 유저레벨인지 나타낸다.

 2. 0~3까지 값을 갖는다(0: 커널 레벨, 3: 유저레벨)

S

 시스템 세그먼트(0)인지 혹은 코드 세그먼트(1)인지 지정

Type

 Type의 첫 비트: 코드 세그먼트인지 데이터 세그먼트인지 구분

 Type 필드의 값 설정에 대한 설명을 보려면 아래 표를 참고하자

G

 세그먼트 단위를 설정한다.

 0일 경우: 세그먼트의 단위를 바이트로 설정한다.

 1일 경우: 세그먼트의 단위를 4KB 단위로 설정한다.

D

 0일 경우: 해당 세그먼트가 16비트

 1일 경우: 해당 세그먼트가 32비트

0

 항상 0으로 지정되어 있는 비트이다.

AVL

 (Available and reserved bits)

 시스템 소프트웨어에 의해 사용되어진다.

*표 내용 참고 문헌: 김범준 저. 2006, 만들면서 배우는 OS 커널의 구조와 원리,Intel Architecture Software Developer’s Manual Volume 3: System Programming

 

 


2. 어셈블리 코드로 GDT 구현 

이제 GDT의 개념에 대해 어느 정도 파악했으니, 실제로 GDT를 어셈블리 코드와 매칭시켜보도록 하겠습니다.

 

GDT 구현 코드GDT 구현 코드

 

위 그림이 GDT 부분을 어셈블리 코드로 구현한 부분입니다. 주황색 네모로 표시된 영역을 볼 때 4개의 디스크립터를 생성했음을 알 수 있습니다. 여기에서 dw는 워드 단위로 데이터로 읽을 것임을 의미하며, db는 바이트 단위를 의미합니다. 그리고 16진수로 표현된 데이터를 비트단위로 쪼개었을 때, 위 그림의 오른편과 같이 각 비트가 의미하는 GDT의 필드와 매칭할 수 있습니다. 이 비트를 가지고 일부 필요한 부분에 한해서 해석하면 다음과 같습니다. (다른 영역의 디스크립터 설명은 생략하겠습니다)


 

<코드 세그먼트 디스크립터>

DPL = 00 : 커널 영역이다.

S = 1: 코드 세그먼트이다.

Type = 1010: (위 테이블을 참조하여) 1(Code), 0(Excute/ Read), 1(non-conforming), 0(non-accessed) 

G = 1: 세그먼트 단위를 4KB로 설정

D = 1: 세그먼트를 32비트로 설정

 

위의 설정한 코드를 좀 더 간략하게 정리하면 아래와 같이 구성할 수 있습니다.

 

GDT 간략화한 구현 코드GDT 간략화한 구현 코드

 

이것으로 GDT 설정을 마쳤습니다. 이제 이를 사용하겠다는 것을 시스템에 알려주면 된다. 이는 다음과 같은 명령문과 같이, GDTR 이라는 레지스터를 통해 GDT의 주소를 LGDT에 저장해주면 됩니다( (R=Register).

 

 lgdt[gdtr]

 

 그러면 정확히 이 코드가 어떻게 동작하는 걸까요?

 

우선 LGDT는 메모리에서 GDT가 가지고 있는 베이스 어드레스(GDT의 시작 주소)와 세그먼트 리미트 값(GDT의 크기)을 인자 값으로 전달 받고, 이를 GDTR 레지스터에 저장하는 역할을 합니다. 즉, 데이터를 전달하기 위해 중간에 한번 거치는 매개역할을 하는 것과 같습니다. 

 

lgdt를 이용한 GDTR에 값 넣기lgdt를 이용한 GDTR에 값 넣기

 

 

이와 같은 과정으로 GDTR에 주소 값을 넣어주기 위해 다음과 같은 코드를 사용합니다.

 

 gdtr:   
        dw gdt_end - gdt - 1    ; GDT의 limit
        dd gdt+0x7C00    ; GDT의 베이스 어드레스

 

위의 문장을 해석해보자면, GDT의 마지막 주소(gdt_end)에서 gdt의 첫번지와 1을 빼면 GDT의 크기(위의 그림에서 16비트)를 알 수 있습니다.

그리고 다음 문장은 GDT의 시작 주소를 프로그램의 시작 주소와 더해줍니다. 왜냐하면 이 전에는 세그먼트:오프셋 형식의 논리주소였던 반면, GDTR 레지스터에는 물리주소를 저장해야 하므로 이를 물리주소로 변환해주기 위해서 입니다.

 

 


3. 세그먼트 셀렉터(Segment Selector)를 이용한 보호 모드에서의 주소 지정 방법


 

여기까지 이제 리얼 모드에서 보호 모드로 넘어가기 위해 GDT를 설정방법을 알아보았습니다. 이제는 보호 모드로 넘어와서 보호모드에서는 어떻게 리얼 모드에서 사용되었던 세그먼트 레지스터의 주소를 지정하는가에 대해 알아보겠습니다.

 

우선, 보호 모드에서는 각 세그먼트의 구성이 달라진다. 다음 그림은 인텔의 개발자 매뉴얼에서 발췌한 그림입니다.

 

 

보호 모드에서의 세그먼트 구조보호모드에서의 세그먼트 레지스터 구성

 

여기에서 오른쪽은 앞서 살펴봤던 64비트 디스크립터 레지스터가 들어있는데, 그 좌측에 세그먼트 셀렉트 레지스터가 추가되었음을 볼 수 있습니다. (각 16비트) 셀렉터? 셀렉터라 하면 영어로 Selector인데, 이것이 보호 모드에서 주소 지정에 사용되는 요소일까요? 좀 더 자세히 살펴보겠습니다.

이 세그먼트 셀렉터의 구성을 살펴보면 아래와 같습니다.

 

 

세그먼트 셀렉터 구조세그먼트 셀렉터의 구조

 




보다시피, 세그먼트 셀렉터는 3가지 필드로 구성되어 있습니다.

 

인덱스: 상위 13비트는 디스크립터를 찾기 위한 인덱스로 구성되어 있습니다.

테이블 지정자: 0으로 지정되면 GDT, 1로 지정되면 LDT를 의미합니다. (LDT 개념은 나중에 살펴보도록 하겠습니다)

요청받은 특권 레벨: 셀렉터의 특권 레벨에 대해 명시합니다. 이 값은 0에서 3의 값을 가질 수 있는데, 가장 강력한 특권 레벨을 설정하려면 0으로 지정해야 합니다(구체적인 내용은 뒤에서 다루도록 하겠습니다).

 

이제 이 셀렉터라는 녀석이 어셈블리 코드로 어떻게 구현되는지 보시죠.

 

세그먼트 셀렉터 구현 코드세그먼트 셀렉터 구현 코드

 

 

위의 3~5라인을 통해 각 디스크립터의 세그먼트 셀렉터 값을 지정하게 됩니다. 그리고 7~10라인은 세그먼트 셀렉터에 값을 넣는 명령을 실행합니다. 그런데, 세그먼트 셀렉터는 16비트라고 했는데, 위의 셀렉터에 저장되는 값은 8비트네요. 물론 나머지 비트는 0으로 채워진다.

 

이제 셀렉터의 값 저장까지 마쳤습니다. 그러면 이제 셀렉터가 GDT의 디스크립터를 어떻게 가리킬까요? 

방법은 세그먼트 셀렉터의 인덱스 값에 8을 곱하는 것입니다.

 

GDT를 찾을 때 물리주소를 사용하는데, 8을 곱해야 인덱스를 구분하여 GDT의 디스크립터를 구분하여 가리킬 수 있습니다. 아래 예를 보시죠.

 

셀렉터가 8일 경우: 1000 --> 인덱스: 1 --> 10진수: 1

셀렉터가 16일 경우: 10000 --> 인덱스: 10 --> 10진수: 2

셀렉터가 24일 경우: 11000 --> 인덱스: 11 --> 10진수: 3

셀렉터가 32일 경우: 100000 --> 인덱스: 100 --> 10진수: 4

...

 

 이를 활용하여 세그먼트 셀렉터를 통해 GDT에서 디스크립터를 찾고 해당 세그먼트 디스크립터 레지스터에 값을 넣는 절차는 다음과 같습니다. (DS 세그먼트를 예로 들었다)

 

세그먼트 셀렉터를 이용한 세그먼트 디스크립터 레지스터에 값 삽입 과정세그먼트 셀렉터를 이용한 세그먼트 디스크립터 레지스터에 값 삽입 과정

 

이와 같은 절차를 통해 예로 든 DS 세그먼트 디스크립터 레지스터에 값을 저장하였습니다.

 

그렇다면, 이제 우리가 보호 모드에서 이 레지스터의 값을 가져오고자 할 때에는 어떻게 찾게 될까요? 해당 세그먼트의 물리주소를 찾으면 되는데, 먼저 다음과 같은 코드를 통해 세그먼트로 접근할 수 있습니다.

 

 

디스크립터 세그먼트 주소 지정 방법디스크립터 세그먼트 주소 지정 방법

 

코드에서 볼 수 있듯이 세그먼트:오프셋 주소를 통해 물리주소를 구하는 절차를 CPU가 진행하게 됩니다. 즉, 여기서 msgPMode는 보호 모드에서 DS의 세그먼트 오프셋 주소임을 알 수 있습니다. 이 msgPMode의 계산된 오프셋 값이 0x65라고 볼 때, 우리는 다음과 같은 절차를 통해 실제 물리주소를 구할 수 있습니다.

 

디스크립터 세그먼트 주소 지정을 위한 계산 방법디스크립터 세그먼트 주소 지정을 위한 계산 방법

 

여기까지 이번 포스팅에서 진행한 절차를 정리하면 다음과 같습니다.

 

1. LGDT를 이용하여 GDT 정보를 GDTR 레지스터에 저장.

2. 보호 모드에서 특정 세그먼트 셀렉터(위 예에서는 DS 세그먼트)를 이용하여 DS 세그먼트 디스크립터 레지스터에 값을 넣음.

3. 이후 이 DS세그먼트 레지스터에 저장된 값을 참조하기 위한 물리주소를 지정.

 

이제 GDT까지 정리하였습니다. 다음 포스팅에서는 드디어 이전에 구현했던 16비트 리얼모드에서 작동하는 부트로더의 코드를 수정하고, 이를 통해 32비트 커널로 넘어가는 코드까지 구현해 보겠습니다. 또 지금까지의 개념을 통해 코드에 대해 심층 분석해 보도록 하겠습니다.

 

 

 

 

 


TAGS.

Comments