$bash

0x06_32비트 보호모드 전환

0x0500 : 기술, 분석 문서/0x0503 : OS 원리와 구조

32비트 보호모드로 전환


리얼모드에서 보호모드로 전환 하려면 6단계가 필요하다.

단계

 모드

 설명

 1

 16비트 리얼모드

 세그먼트 디스크립터 생성

보호 모드 코드와 데이터용 세그먼트 디스크립터 생성

 2

 16비트 리얼모드

GDT 정보 생성

세그먼트 디스크립터의 시작 어드레스와 디스크립터의 전체 크기 저장

 3

 16비트 리얼모드

 프로세서에 GDT 정보 설정

GDTR 레지스터에 GDT의 시작 어드레스와 크기 설정

 4

 16비트 리얼모드

 CR0 컨트롤 레지스터 설정

CR0 컨트롤 레지스터의 PE 비트=1, PG 비트=0

 5

 16비트 리얼모드

 jmp 명령으로 CS 세그머늩 셀렉터 변경과 보호 모드로 전환

jmp 0x08: 보호모드 커널의 시작 어드레스

 6

 32비트 보호모드

 각종 세그먼트 셀렉터 및 스택 초기화

DS, ES, FS, GS, SS 세그먼트 셀렉터와 ESP, EBP 레지스터 초기화

 7

 32비트 보호모드

 보호 모드 커널 실행


세그먼트 디스크립터 생성


세그먼트 디스크립터(Segment Descriptor)는 세그멘테이션 기법(메모리 관리 기법)에서 세그먼트의 정보를 나타내는 자료구조이다. 세그먼트는 메모리 공간을 임의의 크기로 나눈 영역을 의미하며, 세그먼트를 복잡하게 구성할수록 세그먼트 디스크립터의 수도 증가한다. 


세그먼트에 대한 정보를 나타내는 세그먼트 디스크립터는 크게 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터로 나누어진다. 코드 세그먼트 디스크립터는 실행 가능한 코드가 포함된 세그먼트에 대한 정보를 나타내며, CS 세그먼트 셀렉터에 사용된다. 데이터 세그먼트 디스크립터는 데이터가 포함된 세그먼트에 대한 정보를 나타내며, CS 세그먼트 셀렉터를 제외한 나머지 셀렉터에 사용할 수 있다. 스택 영역 또한 데이터를 읽고 쓰는 데이터 영역의 한 종류이므로 데이터 세그먼트 디스크립터를 사용한다.


내가 만들 OS에서 필요로 사용되는 세그먼트는 다음과 같다.

    • 커널 코드와 데이터용 세그먼트 디스크립터 각 1개
    • 커널 코드와 데이터용 세그먼트는 0~4GB까지 모든 영역에 접근할 수 있어야한다.
    • 보호모드용 코드와 데이터에 사용할 기본 오퍼랜드 크기는 32비트여야한다.
    • 보호 기능은 사용하지 않으며, 프로세서의 명령을 사용하는데 제약이 없어야 하므로 최상위 권한 0이어야 한다.

코드 세그먼트 디스크립터와 데이터 세그먼트 타입 설정


코드 세그먼트와 데이터 세그먼트를 설정 하려면 S필드와 타입 필드를 조합해야한다. S필드를 살펴보자.. 코드 세그먼트와 데이터 세그먼트는 세그먼트 디스크립터이므로 간단하게 s필드의 값을 1로 설정한다. 세그먼트 타입은 4비트 크기의 타입 필드를 이요해서 설정한다. 또한 기본적인 세그먼트 타입만 사용하고, 코드 세그먼트는 실행/읽기 타입으로 설정하고 데이터 세그먼트는 읽기/쓰기 타입으로 설정한다. 따라서 코드 세그먼트 타입은 0x0a(Execute/Read), 데이터 세그먼트 타입은 0x02(Read/Write)가 된다.


// 그에 따른 이유는 아래 표를 참고..


세그먼트 영역 설정

OS의 커널 세그먼트 디스크립터는 4GB 전체 영역에 접근할 수 있어야한다. 그러므로 커널용 세그먼트 디스크립터의 기준 주소는 0으로 설정한다. 세그먼트의 기준 주소는 결정했으니 이제 세그먼트의 크기를 설정할 차례이다. 크기 필드는 총 20비트며 20비트로 표현할 수 있는 최댓값은 2^20(=1MB)이다. 크기 필드만으로는 4GB까지의 영역을 표현할 수 없으므로 20비트의 크기를 4GB로 확장할 무엇인가가 필요하다. 이때 사용하는 것이 G필드이며, G필드의 값을 1로 설정하면 크기 필드에 4KB를 곱한 것이 실제 세그먼트의 크기가 된다. 1MB에 4KB를 곱하면 4GB가 되므로 크기 필드와 G필드를 사용하면 메모리 전체 영역을 세그먼트의 입력으로 설정할 수 있다.


기본 오퍼랜드 크기와 권한 설정

보호 모드는 32비트로 동작하므로 기본 오퍼랜드의 크기 역시 32비트로 설정한다. 여러 필드 중에 기본 오퍼랜드의 크기는 D/B 필드가 담당하며, 1로 설정하면 기본 오퍼랜드의 크기를 32비트로 설정할 수 있다. 기본 오퍼랜드의 크기와 관련된 필드가 D/B필드만 있는것은 아니다. IA-32e모드의 64비트 서브 모드 또는 32비트 호환 모드를 설정하는 L필드도 있다. 디스크립터는 보호 모드용이므로 L비트는 0으로 설정한다. 


권한 필드는 보호 모드의 주요 특징 중 하나인 보호 기능에 핵심 역할을 한다. 프로세서는 디스크립터의 권한 필드에 설정된 값과 세그먼트 셀렉터의 권한을 비교하여 접근이 가능한지를 판단하며, x86 프로세서에서 동작하는 운영체제의 대부분도 이 기능를 사용하여 OS의 핵심 부분을 보호하고 있다. 


기타 필드 설정

생성한 세그먼트 디스크립터는 보호 모드로 전환하는 과정에서 사용하므로 유효한 디스크립터라는 것을 알려야한다. 디스크립터가 유효함을 나타내는 필드는 P필드이며 1로 설정하면 해당 디스크립터를 사용할 수 있다. AVL필드는 임의로 사용할 수 있는 필드로 OS에선 별도의 값을 사용하지 않기 때문에 0으로 설정한다.


세그먼트 디스크립터 생성 코드

// 코드 세그먼트 디스크립터와 데이터 세그먼트 디크스립터를 생성하는 내용.



64비트 IA-32e 모드로 전환하려면 반드시 32비트 보호 모드를 거쳐야 한다. 보호 모드는 현대 OS가 제공하는 4GB의 주소 공간, 멀티태스킹, 페이징, 메모리 보호 등의 기능을 하드웨어적으로 지원한다. 목표가 32비트 OS라면 깊게 공부해야한지만, 64비트 OS로 전환하기 위한 임시모드로 쓰기때문에 깊게 공부할 필요가 없다. 보호 모드의 전체 기능에 대해서 살펴보기 보다는 64비트 모드로 전환하는데 필요한 기능을 중심으로 알아보자. 

GDT 정보 생성 

GDT(Global Descriptor Table) 자체는 연속된 디스크립터의 집합이다.  사용하는 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터를 연속된 어셈블리어 코드로 나타내면 그 전체 영역이 GDT가 된다. 

다만 한 가지 제약 조건이 있다면 널 디스크립터(NULL Descriptor)를 가장 앞부분에 추가해야한다는 것이다. 널 디스크립터는 프로세서에 의해 예약된 디스크립터로 모든 필드가 9으로 초기화된 디스크립터이며 일반적으로 참조되지 않는다.

GDT는 디스크립터의 집합이므로 프로세서에 GDT의 시작 어드레스와 크기 정보를 로딩해야한다. 따라서 이것을 저장하는 자료구조가 필요하다.

GDT 정보를 저장하는 자료구조의 기준 주소는 32비트의 크기이며, 데이터 세그먼트의 기준 주소와 관계없이 어드레스 0을 기준으로 하는 선형 주소이다. 따라서 GDT의 시작 어드레스를 실제 메모리상의 어드레스로 변환할 필요가 없다. GDT의 선형 주소는 현재 코드가 실행되고 있는 세그먼트의 기준 주소를 알고 있으므로, 현재 세그먼트의 시작을 기준으로 GDT의 오프셋을 구하고, 세그먼트 기준 주소를 더해주면 구할 수 있다. 현재 코드는 부트로더에 의해 0x10000에 로딩되어 실행되고 있으므로 자료구조를 생성할 때 GDT 오프셋에 아래와 같이 0x10000을 더해주면 선형 주소가 된다.

보호모드로 전환

보호 모드로 전환하려면 GDTR 레지스터 설정, CR0 컨트롤 레지스터 설정, jmp 명령 수행 3단계만 수행하면 된다. 프로세서의 레지스터에 값을 설정하는 작업은 앞에서 살펴본 작업보다 훨씬 간단하다.

프로세스에 GDT 정보 설정

 lgdt 명령어를 2바이트 크기와 4바이트 기준 주소로 된 GDT 정보 자료 구조를 오퍼랜드로 받는다.

// lgdt [ GDTR ]       ; GDTR 자료구조를 프로세서에 설정하여 GDT 테이블을 로드

CR0 컨트롤 레지스터 설정

CR0 컨트롤 레지스터에는 보호 모드 전환에 관련된 필드 외에 캐시(Cache), 페이징(Paging), 실수 연산 장치(FPU) 등과 관련된 필드가 포함되어 있다. 

OS에서 보호모드는 거쳐가는 임시 모드에 불과하므로 세그먼테이션 기능외에는 사용하지 않는다. 따라서 페이징, 캐시, 메모리 정렬 검사, 쓰기 금지 기능을 모두 사용하지 않음으로 설정하면 된다. FPU 역시 쓰지 않으므로 임시 값으로 설정한다. FPU 에 관련된 필드를 제외한 나머지필드는 해당 필드를 설정하는 것만으로 관련 기능을 제어할 수 있다. 하지만, FPU에 관련된 필드(EM, ET, MP, TS, NE)는 서로 연관되어 있으므로 FPU 관련 필드를 설정하는 방법에 대해서 알아보자.

먼저, FPU 내장 여부에 관련된 필드부터 설정하겠다. x86 프로세서에는 FPU가 내장되어 있으므로 EM 필드를 0으로 설정해서 FPU 명령을 소프트웨어로 에뮬레이션하지 않게 하고, ET 필드를 1로 설정한다. 지금은 임시로 초기화를 수행한 것이므로 FPU를 사용하면 정상적으로 작동하지 않는다. 따라서 MP 필드와 TS 필드와 NE 필드를 1로 설정하여 FPU 명령이 실행되었을때 예외가 발생하게 설정한다. 보호 모드에서는 예외에 대해 처리르 ㄹ하지 않으므로 가능하면 실수 연산을 하지 않는것이 좋다.

보호 모드로 전환과 세그먼트 셀렉터 초기화

보호 모드로 전환하기 위한 준비는 끝났다. 남은 것은 32비트 코드를 준비한 후, 한 줄의 어셈블리어 코드로 CS 세그먼트 셀렉터(=레지스터)의 값을 바꾸는 것이다.

16비트에서 32비트로 전환 하려면 BITS 명령어를 사용한다. 

CS 세그먼트 셀렉터를 교체하려면 jmp 명령과 세그먼트 레지스터 접두사를 사용해야한다. 리얼 모드의 세그먼트 레지스터는 세그먼트의 시작 어드레스(기준주소)를 저장하는 레지스터이다. 보호 모드의 세그먼트는 리얼모드와 달리 다양한 정보를 포함하고 있으므로 세그먼트 정보는 디스크립터에 저장하고 세그먼트 셀렉터는 그 디스크립터를 지시하는 용도로 사용한다.

// 이후 화면 출력 내용은..생략...

// 출력이 주된 목표가 아닌 원리 구조 파악으로 진행중.