Project/OStrial in AMAZON

#3 Protected Mode

NONE_31D 2019. 10. 9. 23:51

이전 포스트까지는 16비트로 돌아가는 리얼모드에서 구동했지만, 이제부터는 32비트의 보호모드에서 운영체제를 구동시켜보도록 한다. 


리얼모드는 PC가 부팅할 때, 맨 처음에 동작하는 CPU의 모드로 16비트로 동작한다. 때문에 모든 레지스터가 16비트로 구동됨. 레지스터는 모두 16비트의 한 개의 WORD로 이루어짐, 즉 한번에 0xFFFF 까지밖에 저장이 불가.

 

1. 인텔 CPU에서는 세그먼트 오프셋 방식을 사용하여 주소에 접근하는데, 리얼모드에서는 두 레지스터를 사용해도 16비트씩밖에 사용을 못하기 때문에 오프셋이 정해진다면 해당 오프셋:0xFFFF 까지 밖에 접근을 못한다.

-> 즉, 해당 오프셋 바깥의 주소에는 접근이 불가하며, 접근을 위해서는 FAR JMP를 실행해야 함.

 

2. 리얼모드에서 동작하는 프로그램은 지정한 세그먼트 내부라면 어떤 메모리 영역도 접근이 가능하다. 

- 이는 즉 다른 프로그램이 사용하던 메모리나, 시스템 자체를 위해 할당된 메모리 영역에도 침범이 가능하다는 의미고, 시스템을 위해 할당된 메모리를 침범하게 된다면 운영체제의 오동작을 발생시킬 수도 있다. 

 

3. 아무리 세그먼트 오프셋 방식을 사용하여 메모리에 접근해도, 16비트 레지스터를 쓰는 이상 나타낼 수 있는 한계가 있다.

세그먼트:오프셋 방식을 사용해 최대한 지정할 수 있는 메모리 주소 :
0xFFFF:FFFF = 0xFFFF0 + FFFF = 0x10FFEF

하지만 최근 램은 최소 512MB 이상은 되어 있기 때문에, 리얼모드만으로는 물리 주소를 사용하는데에 한계가 존재한다.

* 책에서는 "최근에는 유저들이 자신의 컴퓨터에 RAM을 512MB~1GB까지 설치하는 경우가 많아졌습니다." 라고 하는데 이 책이 꽤 예전에 나왔다는걸 생각해보면 :/..... 


보호모드(Protected Mode)는 리얼모드와 달리 32비트로 운영이 된다. 보호모드에서는  주소 지정을 하기 위해 리얼모드에서 보호모드로 전환되기 전,  GDT(Global Descriptor Table)을 준비한다.  GDT에서는 세그먼트를 표현하기 위해 위와 같은 정보를 넣어두는데, 하나의 요소를 지칭하여 Descriptor라고 부른다. 이런 descripter가 모였기에 Descipter-Table이라고 부른다.

 

보호모드는 다음과 같은 성격을 가지게 된다.

1. Limit 값이 최대 0xFFFFFFFF까지 지정 가능. 최대 4GB의 영역을 오프셋으로 지정하여 사용 가능. (리얼모드에 비해 메모리 값을 더 넓게 사용)

2. DPL값을 조작하여 유저 단의 프로그램에서 커널 시스템 영역으로 접근하지 못하거나, CS, DS 세그먼트를 분리시켜 코드를 데이터로 읽거나 쓰지 못하도록 방지할 수 있음

3. 보호모드의 GDT의 각 디스크립터에는 세그먼트의 시작 주소를 물리 주소로 지정할 수 있음. 즉, 시작 주소를 물리 주소 값으로 넣기 때문에 1byte단위로 지정이 가능함.

 

GDT의 개괄적 구조

*Base Address : 세그먼트의 시작. 리얼모드에서 CS레지스터에 시작 주소를 넣어 두는 것과 같은 개념. CPU에서는 이 Base Address를 찾아 CS, DS등의 레지스터에 저장한다.

** Limit : 오프셋 레지스터에 저장할 수 있는 최대 값

*** 속성 : 비트값을 저장하여 세그먼트를 어떤 식으로 활용, 보호할 것인지 기재하는 부분

바로 아래에서 자세한 설명 진행될 예정.

 

Descripter의 구조

1. Base Address

- 세그먼트의 시작 주소

- 물리 주소로 하위 16비트, 상위 16비트로 나누어 저장

2. Limit

- 세그먼트의 한계점(크기) 

- 오프셋은 limit을 넘어갈 수 없음. (넘어갈 시, GP fault 발생)

- 20 bit로 구성, 2군데로 나누어 기재.

- 내부의 G비트와 관련됨

3. P비트

- 세그먼트가 존재하는지를 나타냄

- 커널 프로그램에서 메모리 관리 루틴이 사용, 페이징 기능과 관련

- default : 1

4. DPL (Descriptor Privilege Level)

- 2비트 크기

- 세그먼트가 커널의 레벨인지, 유저 레벨인지 표기

- 인텔 x86 기준 0~3까지 존재, 커널 제작시에는 보통 0과 3만 사용

5. S비트

6. Type

- TYPE의 최상위 비트 : 코드/데이터 세그먼트의 구분 지정자

- 마지막 비트 : 액세스 비트. 어떤 프로그램이 세그먼트에 접근했을 경우 CPU에서 해당 비트를 1로 변환함. 하지만 접근이 끝났다고 0으로 clear가 이뤄지지는 않는다.

오히려 커널에서 메모리 관리 중, A비트가 1이 되었는지 조사하거나 액세스 된 segment의 descriptor를 찾아 일정 시간 후 0으로 clear하는 일을 진행함. => CPU의 동작에는 영향을 주지 않음, GDT 초기화시 0이 default.

- 2, 3번째 비트는 세그먼트의 종류에 따라 각각 하는 일이 다름.

 

a. 첫번째 비트가 (11에 해당하는 값) 0인 경우, DATA 세그먼트인 경우

b. 첫번째 비트가 (11에 해당하는 값) 1인 경우, CODE 세그먼트인 경우

7. D 비트

8. AVL

- 시스템 소프트웨어에 사용되는 비트

 

GDTR

  이렇게 GDT가 어디에 만들어져 있는지, 몇 개나 있는지 CPU에서 이해할 수 있도록 등록을 해주어야 함. 이 개념이 GDTR.

GDTR 

- 48바이트

- 0~15 : limit, GDT의 크기 저장

- 16~47 : Base Address, GDT의 시작점 주소 저장

    lgdt[gdtr]

해당 명령문을 통해 GDT를 등록시킴. 명령문에서의 gdtr은 포인터로서, 다음과 같은 구조를 가진다. 

*dw : 16비트 ** dd : 32비트

총 48비트의 값을 갖게 됨.

그래서 위의 명령문에서는 gdtr을 인수로 하여, gdtr 레지스터에 각각 값을 저장하게 된다.

 

1
2
3
    gdtr:
        dw gdt_end - gdt - 1    ; GDT limit
        dd gdt+0x10000          ; GDT base address
cs

 

해당 구문을 통해 프로그램에서 사용할 GDT의 크기를 나타냄. 

두번째 줄에서는 GDT의 마지막 번지에서 첫번째 번지를 빼고, 거기에서 1을 또 빼게 됨

=> gdt_end는 GDT의 맨 끝 주소의 다음 주소를 가리키고 있기 때문

 

마지막 줄에서 gdt의 시작 주소를 물리 주소 값으로 갖고 있는에, gdt 라벨의 주소 값에 0x10000을 더하고 있음.

=> 프로그램이 org 0으로 시작하였기 때문에 프로그램 내부의 모든 주소가 0인 반면, 물리 주소는 0x10000으로 시작

=> GDTR에 들어가야 하는 값은 물리 주소이므로 gdt가 실제 존재하는 물리 주소 값을 계산하여 저장함

 


보호모드의 주소 지정 방법

 

*보호모드에서는 세그먼트 레지스터가 16비트의 한 워드로 구성되어 있었지만, 보호모드로 오면서 각 세그먼트 레지스터가 16비트 셀렉터 레지스터와 64비트의 디스크립터 레지스터로 나뉘어짐.

 

셀렉터 레지스터

* 세그먼트 셀렉터라 함은 세그먼트 레지스터 중 프로그래머가 실제로 다룰 수 있는 부분. 디스크립터 레지스터는 CPU만 사용 가능함. 

세그먼트 셀렉터에 값이 들어가면 CPU는 GDTP 레지스트리에 저장된 GDT의 제일 앞번지를 참조함. 그리고 셀렉터의 인텍스값*8을 진행, 요구한 디스크립터를 찾아냄.  (8을 곱하는 이유 : 인덱스 값 * 바이트, 실제 물리 주소를 나타내기 위함.)

인덱스가 셀렉터에서 13비트를 이용하여 저장되기 때문에 최대로 표기할 수 있는 인덱스 개수는 8192개임. 

 

특정 세그먼트 셀렉터에 값을 넣었을 때, CPU의 동작

1. 세그먼트 셀렉터에서 인덱스를 참조
2. GDTR에 저장된 Base Address 참조
3. 거기서부터 index 만큼을 찾아, 해당 부분의 DPL과 세그먼트 셀렉터의 RPL을 비교
4. 일치한다면 해당 세그먼트 디스크립터 레지스터에 내용을 똑같이 복사.

 

세그먼트와 오프셋을 통해 주소 지정

보호모드에서 세그먼트:오프셋을 사용하여 주소를 지정하면 다음과 같은 동작이 취해짐.

1. 세그먼트 디스크립터 레지스터에서 limit 확인 후,  지정한 오프셋이 limit보다 작을 경우
2. Base Address와 더하여 선형 주소(= 물리 주소)를 구함

* 페이징이 구현되지 않았을 때는 선형주소 == 물리주소

** 페이징이 구현되었다면 선형주소가 페이징 기능을 거친 후 물리주소가 됨.

 

결론적으로, 위의 lea 명령은 0x10065 를 ESI에 넣는 명령이 됨


16비트 리얼 모드 -> 32비트 보호 모드로 이동

 

1
2
3
4
5
6
7
8
9
10
11
12
    cli
 
    lgdt[gdtr]
 
 
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax
 
    jmp $+2
    nop
    nop
cs

 

CPU에는 CR0, CR1, CR2, CR3라는 또다른 레지스터가 존재.

1. CPU의 기능을 바꾸기 위해 프로그래머가 사용

2. CPU에서 현재 상태를 프로그래머에게 알려주기 위하여 사용

3. 프로그래머가 자신의 프로그램을 디버깅 할 때 등에 사용

 

위 코드의 6~8번째 행은 CR0레지스터에 0x00000001 값을 OR 연산으로 추가하는 루틴

 

CR0 의 최하위 비트 : PE비트

- 보호모드임을 표시

- 이 비트가 세트된 후, 운영체제는 보호모드 안에서 동작함. 

* OR 연산을 사용한 이유 : CR0 레지스터 안의 다른 비트값이 손상되지 않기 위해

 

CPU에는 명령을 읽고, 해석하고, 실행되는 유닛이 존재하는데 시분할 기법을 통해, 해당 작업은 동시에 이뤄짐. 그래서 8번까지의 구문 실행을 통해 보호모드에 진입을 했다 하더라도 나머지 유닛에는 리얼 모드용 명령어가 남게 됨. 이 상황에서 보호모드로 리얼모드 명령어를 실행한다면 오류가 생길 가능성 존재. 

=> 해석 유닛과 읽기 유닛에 존재하는 프로그램(명령어)를 지워야 함

 

10~12번 행이 유닛에 존재하는 프로그램을 지우는 루틴. JMP 명령어로 NOP sled를 타는데, 실행 상으로는 2칸을 점프했어도 cpu 내부에서는 해석 유닛과 읽기 유닛에 nop을 남겨 지우게 됨.

 

 

이제 운영체제는 보호모드로 넘어왔으나, 세그먼트 레지스터에는 16비트 값을 포함하고 있음. 다음 코드는 해당 값을 보호모드에 맞게 바꾸도록 진행됨.

1
2
3
4
5
6
    db 0x66
    db 0x67
    db 0xEA
    dd PM_Start
    dw SysCodeSelector
 
cs

이 코드는 FAR JMP와 같이 CS 레지스터에 새로운 세그먼트 값을 저장함. 

CS에는 SysCodeSelector, EIP 레지스터에는 PM_Start: 레이블의 주소 값이 들어가 CPU가 PM_Start: 부터 명령을 실행하게 됨.

 

1~2행은 prefix로서, CPU에게 16비트 명령이 32비트로 바뀌었다던가, 그 반대를 알려주는 표시임

3행의 0xEA는 JMP 명령어를 그대로 기계어로 바꾼 것.

* 0x66 : 16비트 코드에서 32비트의 오퍼랜드로 사용하도록 지정 (또는 그 반대)

* 0x67(Address Prefix) : 16비트 코드에서 32비트 주소값을 사용하도록 지정 (또는 그 반대)

두 prefix는 연달아 사용 가능하고, 16비트에서 사용시 32비트 값을 지정, 32비트에서 사용시 16비트 값을 지정함.

 

위의 코드와 달리, 아래와 같이 16비트 -> 32비트 코드로 점프 가능함

이 이후, 보호모드로 진입하게 됨.


보호모드로 진입 후 해야 할 내용

 

1. 데이터 세그먼트를 각 세그먼트 셀렉터에 저장

- 앞에서 저장된 16비트 값을 갱신

위 코드는 비디오 메모리로 사용하기 위해 만든 디스크립터의 번호를 세그먼트에 저장하고, 비디오 메모리에 문장을 표현하게 함

 

45행에서는 글자를 어디에 표시할지에 대한 계산이고, 일반 식으로 나타내면 다음과 같음

한 열의 칸 수 * 2 * 써야할 열 번호 + 2 * 써야할 칸 번호

소스 코드

#boot.asm

#kernel.asm

(추후 깃허브로 추가 예정)

 

컴파일

 

실행 결과