유저모드 Task Switching(유저모드와 콜 게이트)

아래 3개의 파일을 이용한다.

init.inc

SysCodeSelector equ 0x08

SysDataSelector equ 0x10

VideoSelector equ 0x18

TSSSelector equ 0x20

UserCodeSelector equ 0x28+3        ; 유저모드의 코드 세그먼트 디스크립터는 GDT의 첫번째에서 0x28만큼                                         떨어져 있음. 그리고 셀렉터의 PRL 필드에 2진수 11(10진수 3)을                                           추가하여 이 세그먼트가 유저 영역이라는 표시를 해야한다. 이를 위                                                 해 3을 더하였으며 더하기를 하는 것은 OR 연산과 같은 의미이다. 아래                                                   는 세그먼트 디스크립터의 모양이다.

0x28 -> 40바이트 -> 2진수로 101000, +3을 통해 RPL 값 지정
NULL 디스크립터를 포함하여 6번째 위치
TI가 0 이므로 이 세그먼트 디스크립
터는 GDT에 존재


UserDataSelector equ 0x30+3        유저모드의 데이터 세그먼트 디스크립터는 GDT의 맨 첫번째번지에서
                                        0x30만큼 떨어져 있다. 여기에도 마찬가지로 유저모드 라는 표시를 하                                         기 위해 +3을 해준다.




 boot.asm

%include "init.inc"

[org 0]
jmp 07C0h:start
start:
mov ax, cs
mov ds, ax
mov es, ax
mov ax, 0xB800
mov es, ax
mov di, 0
mov ax, word [msgBack]
mov cx, 0x7FF
paint:
mov word [es:di], ax
add di, 2
dec cx
jnz paint
read:
mov ax, 0x1000 ; ES:BX = 1000:1000
mov es, ax
mov bx, 0
mov ah, 2 ; 디스크에 있는 데이터를 es:bx의 주소로
mov al, 2 ; 2섹터를 읽을 것이다.
mov ch, 0 ; 0번째 Cylinder
mov cl, 2 ; 2번째 섹터부터 읽기 시작
mov dh, 0 ; Head = 0
mov dl, 0 ; Drive=0, A: 드라이브
int 13h ; read
jc read ; 에러가 나면 다시함
mov dx, 0x3F2 ; 플로피디스크 드라이브의
xor al, al ; 모터를 끈다.
out dx, al

cli
mov al, 0x11 ; PIC의 초기화
out 0x20, al ; 마스터 PIC
dw 0x00eb, 0x00eb ; jmp $+2, jmp $+2
out 0xA0, al ; 슬레이브 PIC
dw 0x00eb, 0x00eb
mov al, 0x20 ; 마스터 PIC 인터럽트 시작점
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x28 ; 슬레이브 PIC 인터럽트 시작점
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x04 ; 마스터 PIC에 IRQ 2번에
out 0x21, al ; 스렐이브 PIC이 연결되어 있다.
dw 0x00eb, 0x00eb
mov al, 0x02 ; 스렐이브 PIC이 마스터 PIC의
out 0xA1, al ; IRQ 2번에 연결되어 있다.
dw 0x00eb, 0x00eb
mov al, 0x01 ; 8086 모드를 사용한다.
out 0x21, al
dw 0x00eb, 0x00eb
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0xFF
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0xFB
out 0x21, al
jmp 0x1000:0000
msgBack db '.', 0x67

times 510-($-$$) db 0
dw 0AA55h

kernel.asm

%include "init.inc"


[org 0x10000]

[bits 16]


start:

cld

mov ax, cs

mov ds, ax

xor ax, ax

mov ss, ax

xor eax, eax

lea eax, [tss] ; EAX에 tss의 물리 주소를 넣는다.

add eax, 0x10000

mov [descriptor4+2], ax

shr eax, 16

mov [descriptor4+4], al

mov [descriptor4+7], ah

; 커널 영역에 들어와 실행할 커널 함수의 오프셋을 콜게이트 디스크립터에 적재하는 일을 한다.
 
이 부분은 Protected Mode로 들어오기 전 16비트 코드로 구성되어 있으므로 org 0x10000의
 origin 선언이 적용되지 않으므로 printf 함수의 오프셋에 0x10000을 더한다. 그 후 콜게이트
 디스크립터의 알맞은 필드에 넣는다.

xor eax, eax

lea eax, [printf] ; EAX에 printf의 물리 주소를 넣는다.

add eax, 0x10000

mov [descriptor7], ax

shr eax, 16

mov [descriptor7+6], al

mov [descriptor7+7], ah

cli

lgdt[gdtr]

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

jmp $+2

nop

nop

jmp dword SysCodeSelector:PM_Start

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

; Protected Mode로 들어온 후 스택을 임의의 값으로 지정한다.

lea esp, [PM_Start]    ; PM_Start 번지를 ESP 레지스터에 넣어 커널이 사용하는 스택으로 사용
                             스택에 저장할 때 마이너스 방향으로 진행되므로 PM_Start 보다 작은 쪽 사용
                            PM_Start 번지보다 작은 쪽은 프로그램이 실행되고 더 이상 사용되지 않                                  는 루틴이 존재하므로 스택 영역으로 사용해도 무방하다.


; GDT에 등록된 TSSSelector를 TR레지스터에 적재하는 것을 통해 TSS영역을 사용하도록 한다.
       TSS영역은 한개만 사용한다.(여기서만.. 다음 장에서는 각 태스크마다 TSS처럼 모든 레지스터를 저장해
       두는 메모리 영역을 두어 수동으로 관리하는 방법이 있음)

mov ax, TSSSelector

ltr ax

; ESP에 있는 값을 다시 TSS영역에 있는 tss_esp0의 주소에 넣는다. 이렇게 넣어두면 유저 모드 태스크에서 콜게이트를 거치며 특권 레벨이 변할 때 tss_esp0에 있던 값이 esp 레지스터로 들어가면서 커널 레벨에서 이 값으로 스택을 사용하게 된다.

mov [tss_esp0], esp ; 특권 레벨 0의 스택을 TSS에 지정해 둔다.

lea eax, [PM_Start-256]          ; PM_Start-256의 값을 tss_esp에 넣음. 이 값은 유저모드 태스크에서 사
                                 용하는 스택의 주소
이다.

mov [tss_esp], eax ; 특권 레벨 3의 스택을 TSS에 지정해 둔다.

; 유저모드로의 태스크 스위칭 루틴
 Protected Mode로 들어와 TR 레지스터와 TSS 영역 등을 세팅한 후 유저 모드인 것처럼 가장하여 IRET 명령을 통해 유저모드의 태스크를 실행시킨다.

mov ax, UserDataSelector ; 데이터 세그먼트를 유저 모드로 지정해 둔다.

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

; ESP 레지스터에 PM_Start-256 값을 넣어 유저모드에서 사용할 스택 주소를 넣어둔다.

lea esp, [PM_Start-256]

; 현재 ESP에는 커널모드에서 사용하는 스택 주소가 들어있으며, 이곳에 올 때 까지 PUSH 명령을 사용하지 않았으므로 ESP에 있는 값은 PM_Start 번지 일 것이다. 이 스택에 아래 PUSH 명령을 통해 유저모드의 SS, ESP, EFLAGS, CS, EIP를 차례로 저장하고 IRET 한다.

push dword UserDataSelector ; SS

push esp ; ESP

push dword 0x200 ; EFLAGS

push dword UserCodeSelector ; CS

lea eax, [user_process]               ; EIP에 들어갈 값으로 user_process 함수의 오프셋을 지정

push eax ; EIP

iretd ; 유저 모드 태스크로 점프

printf:

mov ebp, esp                ; esp의 값을 ebp에 복사

push es

push eax

mov ax, VideoSelector       ; VideoSelector를 es에 넣는다.

mov es, ax

mov esi, [ebp+8]

mov edi, [ebp+12]

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop eax

pop es

ret        ; ret 시 jmp $ 명령문이 있는 곳을 가리키게 된다. 그리고 그 자리에서 무한루프를 돈다.

; 유저모드의 태스크가 실행되면 이곳 user_process부터 진행된다.

user_process:

mov edi, 80*2*7

push edi ; 인수를 유저 모드 스택에 저장한다.

lea eax, [msg_user_parameter1]

push eax

call 0x38:0     ; 콜게이트를 통하여 커널 루틴을 호출, 커널모드의 함수 printf로 들어가게 된다.

jmp $        ; 무한루프 부분


msg_user_parameter1 db "This is User Parameter1", 0



gdtr:

dw gdt_end-gdt-1

dd gdt

gdt:

dd 0, 0

dd 0x0000FFFF, 0x00CF9A00

dd 0x0000FFFF, 0x00CF9200

dd 0x8000FFFF, 0x0040920B

descriptor4:

dw 104

dw 0

db 0

db 0x89

db 0

db 0

; 유저모드용 세그먼트 영역을 지정하는 디스크립터 추가(이 디스크립터의 셀렉터 번호는 init 파일에 존재)

dd 0x0000FFFF, 0x00FCFA00 ; 유저 코드 세그먼트

dd 0x0000FFFF, 0x00FCF200 ; 유저 데이터 세그먼트

; GDT에 콜게이트 디스크립터를 만들어 둔다.

descriptor7:

dw 0                    ; 오프셋 0, 이 값은 Protected Mode로 들어오기 전에 설정한다.

dw SysCodeSelector

db 0x02                ; 인수는 2개

db 0xEC

db 0

db 0

gdt_end:


tss:

dw 0, 0 ; 이전 태스크로의 back link

tss_esp0:

dd 0 ; ESP0

dw SysDataSelector, 0 ; SS0, 사용 안함

dd 0 ; ESP1

dw 0, 0 ; SS1, 사용 안함

dd 0 ; ESP2

dw 0, 0 ; SS2, 사용 안함

dd 0


tss_eip:

dd 0, 0 ; EIP, EFLAGS

dd 0, 0, 0, 0

tss_esp:

dd 0, 0, 0, 0 ; ESP, EBP, ESI, EDI

dw 0, 0 ; ES, 사용안함

dw 0, 0 ; CS, 사용안함

dw UserDataSelector, 0 ; SS, 사용안함

dw 0, 0 ; DS, 사용안함

dw 0, 0 ; FS, 사용안함

dw 0, 0 ; GS, 사용안함

dw 0, 0 ; LDT, 사용안함

dw 0, 0 ; 디버그용 T 비트, IO 허가 비트맵

times 1024-($-$$) db 0


컴파일 후 플로피디스크에 삽입 시 아래 그림과 같은 화면이 출력된다.



출력 성공!


보호

CPU는 일부 커널과 함께 모든 유저레벨의 프로그램이 잘 동작하고, 원하는 결과를 얻을 수 있도록 해준다.
Protected Mode로 넘어온 후에는 위에서 이야기한 바대로 여러 유저 프로그램이 같이 동작하기 때문에 CPU는 각 명령어마다 여러 가지 체크를 하고 합당한 명령만 실행시키고, 합당하지 않으면 폴트를 발생시켜 커널이나 유저 프로그램 자신이 이 에러를 다시 조정하도록 한다.
이러한 체크에는 4가지가 존재한다.
    1. Limit 체크
    2. Type 체크
    3. 특권 레벨 체크
    4. 명령 세트 체크


1. CPU의 체크 포인트

1.1 Limit 체크

세그먼트 디스크립터 내 G비트가 0일 때에는 Limit를 0xFFFFF(1MB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0으로 지정해도 되고, 0xFFFFF로 지정해도 된다는 의미이다.
세그먼트 디스크립터 내 G비트가 1일 때에는 Limit를 0xFFFFFFFF(4GB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0xFFF로 지정해도 되고, 0xFFFFFFFF로 지정해도 된다는 의미이다.
세그먼트 영역에서 유효 Limit는 전체 세그먼트 영역의 크기에서 1바이트를 뺀것과 같다. 아래의 경우 CPU는 일반 보호 예외(#GP)를 발생시킨다.
    1. 오프셋이 유효 Limit보다 큰 바이트
    2. 오프셋이 유효 Limit-1보다 큰 워드
    3. 오프셋이 유효 Limit-3보다 큰 더블워드
    4. 오프셋이 유효 Limit-7보다 큰 쿼드워드

1.2 Type 체크

세그먼트 디스크립터에는 S비트와 Type 필드가 있다. S비트가 0이면 Type 필드가 시스템 Type이라는 것이고, 1이면 코드나 데이터 세그먼트의 Type이라는 뜻이다.


예를 들어 P비트가 1, DPL이 00, S가 0이고, 32비트 인터럽트 게이트일때 디스크립터의 한 부분에 0x8E가 들어가고, 32비트 TSS였다면 0x89, 32비트 TSS의 B비트가 1로 세트되었을 때 0x8B가 되는 것을 기억하면 된다.

CPU는 세그먼트 셀렉터와 세그먼트 디스크립터를 조작할 때 여러가지 시점에서 Type 정보를 조사한다. 예를 들어 아래와 같은 경우가 Type 체크되는 대표적인 예이다.

1. 세그먼트 셀렉터가 세그먼트 레지스터에 로드될 때 CS에는 코드 세그먼트의 셀렉터만이 로드될 수 있다.
    MOV AX, 0x08
    MOV CS, AX

2. 디스크립터가 세그먼트 레지스터에 이미 로드되어 있는 세그먼트에 명령이 액세스 할 때
    -> 코드 세그먼트 영역에는 데이터의 쓰기가 금지된다.
    -> 읽기 전용 데이터 세그먼트에 데이터의 쓰기가 금지된다.
    -> 코드 세그먼트 영역에 읽기 가능한 플래그가 설정되지 않은 상태에서는 이 영역을 읽어 들일 수 없다.

3. CALL JMP 명령의 오퍼랜드에 셀렉터가 있을 때 그 셀렉터에 대한 디스크립터의 Type 필드를 조사한다.

TSS 셀렉터를 오퍼랜드로 한 CALL JMP 명령에서는 그 셀렉터에 대한 디스크립터가 TSS용인지를 자동 체크한다. 또는 IRET 명령이 내려졌을 때 현재 실행중인 태스크의 NT비트가 1로 세트되어 있으면 이에 대한 TSS영역에서 "이전 태스크로의 백링크"가 TSS용인지를 체크한다.

GDT의 맨 처음 디스크립터인 NULL 디스크립터를 CS나 SS에 로드하려고 할 때 일반보호 예외 발생

DS, ES, FS, GS에 로드는 가능하나 로드된 셀렉터를 사용하여 세그먼트 영역에 접근하려고 해도 일반 보호 예외(#GP)가 발생


2. 특권레벨

CPU의 특권레벨은 0~3으로 4개가 있으며, 0과 3의 2개 레벨만을 사용한다. 낮은 레벨의 태스크가 높은 레벨의 데이터를 참조하려고 하면 일반 보호 에러(#GP)가 발생한다.

특권레벨을 이해하기 위해서는 CPL, DPL, RPL 세 개의 레벨을 알아야 한다.

CPL(Current Privilege Level)
CPL은 현재 실행되고 있는 태스크의 특권 레벨로, CS, SS 셀렉터 레지스터의 0, 1번째 비트에 있는 수이다. 프로그램이 다른 특권 레벨의 코드 세그먼트로 제어 이행되면 CPU는 CPL을 변경한다.

DPL(Description Privilege Level)
DPL은 디스크립터에 기재된 DPL 값이고 2비트로 표현된다. 디스크립터를 통한 세그먼트 영역으로의 모든 접근에서 항상 CPL과 DPL의 관계가 체크된다. 예를 들어 현재 낮은 레벨의 CPL을 가지고 있는데, 높은 레벨의 데이터 세그먼트 영역에 접근을 시도한다면 CS의 0, 1비트(CPL)와 세그먼트 디스크립터에 있는 DPL 값을 비교할 것이다. 이러한 상황에서는 체크 후 물론 불가능하다는 판단이 내려진다.

RPL(Requested Privilege Level)
특권레벨 3인 프로세스가 콜게이트(낮은 특권 레벨에서 높은 특권 레벨의 루틴을 사용할 수 있게 하는 관문)를 통해 특권레벨 0에 있는 루틴을 실행 할 때가 있다. 이때 일시적으로 특권 레벨 0으로 들어가기 때문에 특권레벨 0의 데이터 영역에 접근 가능하다. 이러한 현상을 교모히 이용하는 프로그램도 존재할 가능성이 있다. RPL 값은 이러한 상황을 막기 위해 OS 커널이 사용하는 값이다. 예를 들어 콜게이트에 연결되어 있는 루틴에 이 루틴이 어디에서 불려졌는지를 기록하는 방법이 있다. 특권레벨 3인 프로세스가 콜게이트를 통해 이 루틴을 불렀다면 데이터 세그먼트 셀렉터에 특권레벨 3에서 불려졌다는 것을 표시한다. RPL은 누가 요구 했는가를 나타내는 값이다.


콜게이트

낮은 특권 레벨의 프로그램이 실행 도중 높은 특권 레벨로 변경되는 수단은 대표적으로 인터럽트, 예외, 콜게이트가 있다. 이 중 하드웨어 인터럽트와 예외에서는 낮은 특권 레벨 프로그램이 자신의 의지와는 상관없이 특권 레벨의 변경이 이루어지고, 소프트웨어 인터럽트와 콜게이트는 낮은 특권 레벨의 프로그램이 자신의 의지에 의해 높은 특권 레벨의 루틴을 잠깐 사용하는 것이다.
콜게이트도 다른 세그먼트 디스크립터와 함께 GDT 테이블에 디스크립터로서 포함되며, 또 하나의 세그먼트 정의라고 생각하면 된다. 낮은 특권 레벨의 프로그램이 높은 특권 레벨의 프로그램의 일부분을 사용하기 위한 창구 세그먼트이다. 디스크립터의 생김새는 아래와 같으며 "인수의 개수" 칸과 Type 이외의 부분은 다른 디스크립터와 비슷하다.

위 디스크립터를 GDT에 지정해 놓고, 유저 프로그램은 코드 셀렉터의 사용법과 마찬가지로 이 디스크립터를 셀렉터로 선택하여 JMP나 CALL 명령을 내린다. 오프셋은 디스크립터에 지정되어 있으므로 아무 숫자나 사용하여도 디스크립터에 지정된 오프셋의 루틴이 수행된다.


코드와 데이터의 특권레벨 관계

특권 레벨 간 JMP 명령은 불가(굳이 사용해야 한다면 콜게이트(Call Gate)를 사용하여 이루어 져야 한다.
CALL 명령은 특권 레벨 간의 이동 가능, 항상 낮은 특권 레벨에서 높은 특권 레벨에 대하여 이루어져야 하고, RET 명령은 높은 특권 레벨에서 낮은 특권 레벨에 대하여 이루어져야 한다. 특권 레벨 0의 코드가 특권 레벨 3의 코드를 불러낼 이유가 없기 때문이다.

특권레벨 0에서 동작하는 동안 CS(CPL) 셀렉터의 0, 1비트에는 00이 들어가 있다. 특권 레벨 3에서 동작하는 동안의 CS 셀렉터에는 0, 1비트에 11(10진수 3)이 들어가 있게 된다.

특권레벨 3에서 CALL 명령으로 특권레벨 0의 루틴을 불러내면 CS 셀렉터의 0, 1비트에 00, CPL은 0이며
특권레벨 0에서 RET 명령으로 특권레벨 3의 루틴으로 돌아갈 시 CS 셀렉터의 0, 1비트에 11(3), CPL은 3이다. (같음)
한가지 예외가 있다면 Conforming 세그먼트 일 경우, 특권레벨 0의 루틴을 불러내도 CPL의 값이 그대로 3인 채로 실행 되었다가 RET 명령으로 돌아온다. Conforming 세그먼트는 콜게이트와 관련있다.


유저 테스크가 콜 게이트 등을 사용하여 커널모드의 루틴을 불러내는 경우

1. CPU는 이 태스크의 TSS 영역에 있는 SS0, ESP0의 값을 참조하여 커널모드의 스택에 현재 유저 태스크가 사용하고 있는 SS, ESP, 그리고 유저 태스크가 현재 진행 중인 루틴의 주소인 CS, EIP를 차례로 PUSH한다. 그리고 CPU의 SS, ESP 레지스터에는 SS0, ESP0의 값을 각각 넣는다.
2. 콜게이트에 지정된 커널 모드의 루틴 주소로 점프 후 실행한다.
3. RET 명령이 내려질 시 현재 커널 모드의 스택에 저장된 SS, ESP, CS, EIP를 POP하여 CPU의 각각 레지스터에 복원하여 유저모드 태스크로 돌아간다. 이때 먼저 CS를 확인한 후 현재 커널 모드보다 낮은 특권 레벨이면 SS와 ESP를 POP한다.
호출 구문에 인수가 2개 있을 시 인수의 개수에 2를 기입해야 한다. GDT에 있는 콜게이트의 디스크립터의 "인수의 개수" 칸에 2가 들어있다면 유저 레벨의 스택에서 2개를 커널 모드의 스택에 복사한다.


인터럽트 예외가 발생하였을 때의 스택

EFLAGS가 추가된 것만 빼면 콜게이트의 경우와 비슷하다.
먼저 커널모드인 특권레벨 0에서 인터럽트 발생 시 스택 변경이 일어나지 않는다. 특권 레벨 0에서는 굳이 시스템을 보호할 필요가 없다. 그러므로 특권 레벨 0에서 사용하고 있던 SS, ESP 그대로의 값을 사용하여 스택에 EFLAGS, CS, EIP를 PUSH 한다.

유저레벨 태스크의 동작 중에 인터럽트나 예외가 발생하면 유저레벨 태스크의 TSS 영역에서 SS0, ESP0을 CPU의 SS, ESP 레지스터에 복사한다. 이로써 커널 모드의 스택을 사용하게 되는데, 커널모드 스택에 콜게이트의 경우와 같이 유저모드의 태스크가 사용하던 SS, ESP, EFLAGS, CS, EIP 등의 레지스터 값을 PUSH 하여 저장한다. 그 후 커널모드에 있는 인터럽트 핸들러 루틴으로 점프하고, 인터럽트 처리가 끝나면 IRET을 사용하여 인터럽트가 걸린 유저모드로 돌아오는데, 이때 스택에 있는 값들을 POP하여 CPU의 각각의 레지스터에 다시 복원한다.

TSS 영역에 SS0, ESP0, SS1, ESP1 등 레벨 별로 존재(SS3, ESP3은 존재하지 않음)하며, 커널모드 스택, 유저모드 스택 조정이 가능하다.(5장 Task Switcing 참조)


Task Switching(CALL 이용)

이전의 소스 중 kernel.asm 파일 내 jmp 문을 call 및 Process 루틴 하단의 jmp문을 iret 으로 변경하여 준다. (변경 시 Process2 명령어가 실행되고 이상하게 계속 재부팅이 된다..??)

여튼 .. 변경 후 원리를 알아보자..

GDT에 있는 TSS 디스크립터에 대하여 셀렉터를 사용한 CALL 명령에서 태스크 스위칭이 일어나고 또한 IRET 명령을 통해서도 태스크 스위칭이 일어난다.
이전 장에서 TSS 세그먼트 디스크립터에 포함된 Type 필드 내 1 0 B 1 이 존재하는 것을 배웠다. 그리고 EFLAG 레지스터의 14번 비트는 NT비트로 사용한다.

NT비트(Nested의 약자)는 IRET 명령을 행할 때 이것이 인터럽트 핸들러의 IRET인지, 태스크 스위칭되어 다시 이전의 태스크로 돌아가는 의미의 IRET인지를 CPU가 구별할 때 사용한다.
LTR 명령은 해당 TSS 디스크립터의 B 비트를 1로 세트한다. 그리고 CPU는 현재 실행되고 있는 태스크의 비트는 항상 1이라고 인식한다. CALL 명령에 의한 태스크 스위칭이 일어날 때 신태스크의 EFLAG에 있는 NT비트가 1로 세트된다. 이때 B비트도 1로 세트된다. 또한 TSS영역에 있는 이전 태스크로의 백링크 칸에 구태스크의 TSS 디스크립터 셀렉터를 저장해 둔다. 구태스크의 B비트는 1인채로 남아 있다.
IRET 명령을 사용하면 이전 태스크로 스위칭 된다. IRET 명령이 실행되기 위해 현재 실행중인 태스크의 NT비트가 반드시 1이어야 한다. 인터럽트의 IRET 명령과 구별하기 위함이다. 그리고 이전 태스크의 B비트도 1이어야 한다. IRET은 현재 태스크의 TSS 영역에 있는 이전 태스클의 백링크를 사용하여 이전 태스크를 찾는다. 이전 태스크로 돌아갈 때 현재 실행중인 태스크의 NT 비트는 0으로 클리어 되고, B비트도 0으로 클리어 된다.

CALL 명령과 IRET 명령에 의한 태스크 스위칭이 B비트, NT비트로 복잡한 규정이 설정되어 있다. 이것은 태스크 스위칭에 어느 정도 질서를 마련하기 위함이다.

TSS3에서 CALL TASK2:0과 같은 형식으로 이전 태스크로 돌아갈 수 없다. 왜냐하면 TASK2의 B비트가 1이 된 상태이기 때문이다. 이처럼 B 비트를 둔 이유는 CALL 명령을 사용한 태스크 스위칭에서는 태스크끼리 연결되어야 하고, CALL 명령을 사용해서 이전 태스크로 돌아가지는 못하게 하려는 뜻이다.

TASK1 실행 도중 CALL 명령을 통해 TASK2로 태스크 스위칭을 한다. TASK2 실행 도중 인터럽트가 발생한다. CPU는 인터럽트가 발생한 순간 CS, EIP와 함께 EFLAGS를 PUSH하여 스택에 저장한다. TASK2의 NT비트가 EFLAGS에 있으므로 이때 함께 저장된다. 그리고 CPU는 EFLAGS를 초기화하고, 인터럽트 핸들러를 실행한다. 인터럽트 핸들러에서 사용하는 EFLAGS에 있는 NT비트도 0으로 초기화 되는 것이다.
커널모드의 EFLAGS에는 NT비트가 0이므로 인터럽트 핸들러 실행을 마치고 IRET 할때에는 태스크 스위칭이 아닌 인터럽트 핸들러에서 인터럽트가 발생될 당시의 루틴으로 돌아가는 IRET의 의미가 된다. IRET 될 때에 스택에 저장되어 있던 CS, EIP, EFLAGS등이 다시 복원된다. 복원된 EFLAGS에는 TASK2가 사용하던 NT비트가 포함되어 있고, 이 비트가 1인 상태로 복원된다. TASK2가 실행을 마치고 IRET을 통해 이전 태스크로 스위칭할 때에는 NT비트가 1이므로 IRET이 태스크 스위칭을 위한 IRET의 의미가 된다. 그리하여 TASK1로 무사히 태스크 스위칭을 할 수 있다.

태스크 스위칭과 인터럽트 핸들링은 다른 방식으로 이루어 진다는 것을 알 수 있다.
여기까지 알 수 있는 것은 CALL 명령과 IRET 명령으로 태스크 스위칭을 하는 것은 태스크들이 하나의 체인으로 묶여 있고, CALL 명령으로 스위칭 된 태스크들은 CALL 명령을 통해 새로운 태스크로 스위칭 될 수 있으나 반드시 같은 순서로 IRET 명령을 통해 이전 태스크로 돌아오게 되어야 한다는 것이다. (CALL 명령은 커널 레벨에서 이루어지고, IRET은 태스크 자신의 루틴에 포함된다.)
이러한 방법은 태스크 관리에 있어 비선점 프로세스 관리에 해당된다. 하나의 프로세스가 다른 프로세스로 스위칭되고 난 후 구 프로세스는 신프로세스가 끝나기 전에는 절대로 실행이 재개될 수 없다.

JMP 명령에 의한 태스크 스위칭에 대해선 아래와 같다.

1. JMP 명령을 통해 TASK2로 점프한다. 이때 TASK1의 B비트는 0이건 1이건 무조건 0으로 세트한다. 그리고 태스크 스위칭 된 이후 TASK2의 B비트는 0에서 1로 세트된다. JMP 명령을 통한 태스크 스위칭은 신태스크의 B비트가 0인 상태에서만 가능하다. TASK2의 NT비트는 무조건 0이 된다.
2. 실행되고 있는 TASK2를 JMP 명령을 통해 TASK1로 스위칭을 하면 TASK2의 B 비트는 0이 되고, TASK1로 스위칭 된 후 TASK1의 B비트는 0에서 1이 된다. TASK1의 NT비트는 무조건 0이 된다.

태스크 스위칭 하는 동안 구태스크의 B비트가 0이 되므로 태스크 스위칭을 JMP 명령만으로 행한다면 B비트가 1이 되어 있는 것은 항상 현재 실행되고 있는 태스크뿐이라는 것을 알 수 있다. 태스크 스위칭 후 신태스크의 NT비트는 항상 0이므로 CALL 명령과는 달리 태스크끼리의 구속성이 없다. JMP 명령으로 태스크 스위칭을 하는 것은 상당히 자유스럽다는 것을 알 수 있다.

CALL 명령으로 태스크 스위칭을 하는 방법비선점형 태스크 스위칭을 위한 기능이다. JMP 명령은 유저 모드의 태 스크로 스위칭 하는 데에 약간의 문제점을 가지고 있다.

Task Switching(태스크 스위칭)

태스크 스위칭의 방법을 알아보기 위해 아래와 같이 소스를 짠다.


boot.asm


init.inc


kernel.asm

%include "init.inc"


[org 0x10000]

[bits 16]


start:

cld

mov ax, cs

mov ds, ax

xor ax, ax

mov ss, ax

; TSS 디스크립터에 Base Address를 넣는 부분이다.

xor ebx, ebx

lea eax, [tss1] ; EAX에 tss1의 물리 주소를 넣는다.

add eax, 0x10000    ; 더하는 이유는 디스크립터의 Base Address는 물리주소 값이 들어가야 하기 때문
                           이 소스부분이 16비트 코드이므로 오프셋을 나타내는 레지스터도 16비트
                           따라서 org[0x10000] 이라는 origin이 오프셋에 적용되지 않기 때문이다.

mov [descriptor4+2], ax

shr eax, 16

mov [descriptor4+4], al

mov [descriptor4+7], ah

lea eax, [tss2] ; EAX에 tss2의 물리 주소를 넣는다.

add eax, 0x10000

mov [descriptor5+2], ax

shr eax, 16

mov [descriptor5+4], al

mov [descriptor5+7], ah

cli

lgdt[gdtr]

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

jmp $+2

nop

nop

jmp dword SysCodeSelector:PM_Start

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

lea esp, [PM_Start]


mov ax, TSS1Selector

ltr ax              ; LTR 명령은 CPU에 있는 TR 레지스터에 TSS 디스크립터의 셀렉터 값을 넣는 명령어

   ; 위와 같이 설정 시 현재 진행중인 태스크가 스위칭이 되어 저장되어야 할 때에는 이 TSS     영역을 저장하라 는 뜻이다. 또한 LTR 명령은 태스크 스위칭 구현에 이어 필수이다. 

lea eax, [process2]    ; Process2 라는 서브루틴이 존재하며, 이 루틴은 하나의 태스크로 다룰 것이다.      

mov [tss2_eip], eax   ; TSS2 영역의 EIP에 process2 루틴의 첫 번지를 넣어두어 태스크 스위칭이 일어났을
                            때 Process2: 번지부터 실행되도록 해둔다.

mov [tss2_esp], esp


jmp TSS2Selector:0 ; 다시 태스크 스위칭이 되면 이곳으로 돌아온다.
                           ; 세그먼트 셀렉터는 스위칭을 한 다음 TSS의 TSS 디스크립터 셀렉터 값을 사용한다.                            ; 오프셋은 아무 숫자나 사용 가능, CPU의 펌웨어 구조가 태스크 스위칭을 행할 때 오                             프셋은 아무 숫자나 사용해도 되도록 되어 있다. (오프셋은 별 의미 없음)                            ; 위 명령 실행 시 이 시점에서 tss2의 EIP에는 process2: 의 주소가 들어있기 때문에                              process2 루틴의 처음부터 실행하게 된다.

mov edi, 80*2*9

lea esi, [msg_process1]

call printf

jmp $


printf:

push eax

push es

mov ax, VideoSelector

mov es, ax

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop es

pop eax

ret

process2:        ; Process2 루틴이다. 이곳에 들어왔다면 태스크 스위칭이 된 상태이다. 태스크 스위칭이 일어나
                  면 CPU의 TR 레지스터에는 새로운 태스크의 TSS 디스크립터 셀렉터의 값이 들어간다.
                  이 의미는 이제부터 태스크 스위칭이 일어나면 이곳으로 모든 레지스터 값을 저장하라 라는 뜻이
                 다.

mov edi, 80*2*7

lea esi, [msg_process2]    ; 문장 표시

call printf

jmp TSS1Selector:0        ; tss1 태스크로 스위칭 한다. 이 명령 직후 현재 TR 레지스터에 있는 TSS 디스크
                                립터 셀렉터
와 관련된 TSS 영역(여기에서는 tss2)에 모든 레지스터를 저장하고,
                                TSS1Selector와 관련된 TSS 영역에서 모든 레지스터 값을 꺼내서 CPU에 복원
                                시키고, TR 레지스터에 TSS1Selector 값을 저장한다.


msg_process1 db "This is System Process 1", 0

msg_process2 db "This is System Process 2", 0


gdtr:

dw gdt_end-gdt-1

dd gdt

gdt:

dd 0, 0

dd 0x0000FFFF, 0x00CF9A00

dd 0x0000FFFF, 0x00CF9200

dd 0x8000FFFF, 0x0040920B

descriptor4:

dw 104    ; TSS 의 Limit 가 104(0x68)로 되어있음. TSS 디스크립터에서는 Limit 값이 항상 0x67이상의 값
                가지고 있어야 한다. 그렇지 않을 시 태스크 스위칭 시 무효 TSS 예외(#TS)가 발생한다.

dw 0

db 0

db 0x89    ; P 비트가 1, DPL이 00, 고정된 0, Type이 1001이라는 것을 알 수 있다. 태스크 스위칭은 항상
                  커널 레벨에서 이루어 지도록 하는 것이 보통이므로 DPL은 00으로 해둔다.

db 0        ; 소스상에는 Base Address에 TSS 영역의 시작주소를 넣는데, 0으로 되어 있지만 프로그램으로
                   Base Address를 넣도록 하였다. 그 부분이 상단의 Start의 xor ebx, ebx 부분이다.

db 0

descriptor5:

dw 104

dw 0

db 0

db 0x89

db 0

db 0

gdt_end:


tss1:

dw 0, 0 ; 이전 태스크로의 back link, 이전에 동작하던 프로그램의 TSS 영역의 세그먼트 셀렉
                            터 값이 들어간다.
TSS영역은 GDT에 있는 TSS 세그먼트 디스크립트와 한쌍을 이룸
                            2개의 태스크를 관리하기 위해 2개의 TSS, TSS 세그먼트 디스크립트 쌍을 지정함.
                            이 디스크립터를 셀렉터로 지정하여 JMP, CALL 명령으로 태스크 스위칭을 한다.
                            JMP 명령은 해당되지 않으나, CALL 명령을 통해 태스크 스위칭다음 태스크는 자
                           신의 TSS 영역에 이전 태스크의 TSS 디스크립터의 셀렉터 값을 저장
해 두었다가 자신
                            은 IRET 명령으로 프로그램을 마치고 CPU는 현재 태스크의 TSS영역에서 이전 태스
                            크로의 back link 값을 사용하여 이전 
태스크로의 스위칭을 행하게 된다.

dd 0 ; ESP0

dw 0, 0 ; SS0, 사용안함

dd 0 ; ESP1

dw 0, 0 ; SS1, 사용안함

dd 0 ; ESP2

dw 0,0 ; SS2, 사용안함 

dd 0, 0, 0 ; CR3, EIP, EFLAGS

dd 0, 0, 0, 0 ; EAX, ECX, EDX, EBX

dd 0, 0, 0, 0 ; ESP, EBP, ESI, EDI

dw 0, 0 ; ES, 사용안함

dw 0, 0 ; CS, 사용안함

dw 0, 0 ; SS, 사용안함

dw 0, 0 ; DS, 사용안함

dw 0, 0 ; FS, 사용안함 dw 0, 0 ; GS, 사용안함

dw 0, 0 ; LDT, 사용안함

dw 0, 0 ; 디버그용 T비트, IO 허가 비트맵

tss2:

dw 0, 0 ; 이전 태스크로의 ack link

dd 0 ; ESP0

dw 0, 0 ; SS0, 사용안함

dd 0 ; ESP1

dw 0, 0 ; SS1, 사용안함

dd 0 ; ESP2

dw 0, 0 ; SS2, 사용안함

dd 0


; 아래 소스 부분은 tss2에 지정된 tss2_eip 번지와 tss2_esp 번지를 나타낸다.

tss2_eip:

dd 0, 0 ; EIP, EFLAGS (EFLAGS=0x200 for ints)

dd 0, 0, 0, 0

tss2_esp:    ; tss2_esp에 원래 유저영역의 스택주소가 저장되어야 하나, 커널모드끼리의 태스크 스위칭인 이유도
             있으며, 소스의 설명을 위한 편의상 mov [tss2_esp], esp 명령이 실행될 때의 esp 스택 포인터의 값을
             넣는다.

dd 0, 0, 0, 0 ; ESP, EBP, ESI, EDI

dw SysDataSelector, 0 ; ES, 사용안함

dw SysCodeSelector, 0 ; CS, 사용안함

dw SysDataSelector, 0 ; SS, 사용안함

dw SysDataSelector, 0 ; DS, 사용안함

dw SysDataSelector, 0 ; FS, 사용안함

dw SysDataSelector, 0 ; GS, 사용안함

dw 0, 0 ; LDT, 사용안함

dw 0, 0 ; 디버그용 T 비트, IO 허가 비트맵

times 1024-($-$$) db 0


컴파일 시 아래 그림과 같이 프로세스가 태스크 스위칭 되어 출력되는 것을 확인할 수 있다.

두개의 tss영역을 사용하여 두 개의 프로그램을 태스크 스위칭한다. 진행되던 루틴을 태스크 스위칭 시켜 새로운 프로그램이 CPU에서 동작하게 하다가 다시 본래 진행되던 루틴으로 태스크 스위칭 한다. Process 2 부분은 새로운 프로그램이 화면에 표시한것, Process 1 부분은 새로운 프로그램에서 태스크 스위칭을 하여 본래 진행되던 프로그램을 다시 CPU에서 동작하게 하여 화면에 나타낸 문자열이다. 문자열을 나타내고 프로그램은 멈추게 된다.


중요 사항 정리

1. Protected Mode의 태스크 스위칭은 CPU의 비효율적인 시간 낭비를 보완하기 위해 CPU 타임을 FULL로 사용할 수 있게 해준다.
2. 우리가 사용하는 Intel 80286 이상의 CPU는 Protected Mode에서의 태스크 스위칭을 CPU 레벨에서 지원해준다.
3. 선점형 방식 태스크 스위칭은 프로그램이 실행되는 동안 어떤 상황에 있든지 관계 업이 그 프로그램을 일단 정지시키고 다른 프로그램이 이전에 실행했던 곳부터 다시 실행되도록 하는 방식이다.
4. 위 구현을 위해 CPU에서 수행되는 프로그램의 모든 레지시터 값을 일단 보존시키고, 이전에 수행되었다 저장되었던 프로그램의 모든 레지스터 값들을 CPU에 옮겨 놓아, 멈추었던 부분부터 다시 재개하도록 해야한다.
5. 프로그램이 재개되면 보존되어 있던 CS에 있는 코드 세그먼트EIP가 복원되어 그자리부터 프로그램이 실행될 것이고, SS:ESP도 복원되어 스택 영역을 참조하고, EAX, EBX, ECX, EDX 등 범용 레지스터들도 값이 복원된다.
6. 태스크 스위칭을 구현하기 위해서는 먼저 RAM상에 모든 레지스터 값들이 보존될 영역을 만들어 놓아야 한다. 이 영역을 TSS(Task State Segment)라고 한다.
7. TSS를 지정하는 TSS 디스크립터가 GDT에 지정되어 있어야 한다. TSS 생김새는 아래와 같다.

TSS는 CPU의 거의 모든 레지스터 값들을 저장할 수 있게 되어있다. GDTR, IDTR, CR0~CR2 등의 모든 태스크가 공통으로 사용하는 레지스터를 제외하고, 각 태스크가 사용하는 모든 레지스터를 포함하도록 되어 있다. RAM 에서의 번지가 그림처럼 아래에서 위로 증가하고, 소스에서는 위에서부터 아래로 증가한다. (혼동 주의)

ESP0, SS0

유저 모드(레벨 3) 태스크가 실행 중 커널모드(레벨 0)로 태스크 스위칭이 행해졌을 때 스택 값이 바뀌어야 한다. 그 이유는 유저모드와 커널모드에서 스택을 같이 사용한다면 프로그램의 실행이 엉키기도 할 뿐더러 유저모드에서 커널모드의 데이터를 읽고 쓰는 행위가 가능해지므로 커널의 보안 기능이 나빠지게 된다. 그래서 TSS 영역에 ESP0, SS0, ESP1, SS1, ESP2, SS2와 같이 CPU가 사용하는 시스템 레벨별로 스택이 따로 존재한다. 이중 우리는 레벨 0과 레벨 3망늘 사용한다. ESP3, SS3은 없다. 유저 레벨 스택은 TSS의 ESP 칸과 SS칸에 저장한다.

CR3
페이징 구현과 관련 있다. 차 후 설명함

디버그용 T 비트
유저 레벨 태스크를 디버깅 할 때, 브레이크 포인트를 걸어두고 한 스텝씩 진행시켜가며 프로그램 동작을 확인해야 할 경우가 있다. 물론 디버그 도중 다른 태스크가 존재하고 태스크 스위칭이 빈번히 행해지기 때문에 디버깅하고 있는 태스크가 태스크 스위칭 되기 전에 이 태스크는 디버깅 중이었다는 표시를 이곳 T 비트에 해둔다.

I/O 허가 비트맵
유저 레벨 태스크는 주변장치를 마음대로 사용 불가능이다. I/O 허가비트맵이라는 것으로 사용할 수 있는 I/O 장치와 사용할 수 없는 I/O 장치를 구분해야 한다. 이 또한 RAM의 한 영역에 표시해야 한다. 이 표시해 둔 영역의 시작 주소를 TSS의 I/O 허가 비트맵 칸에 넣어둔다.

TSS와 한쌍을 이루는 TSS 세그먼트 디스크립터는 아래와 같다.

위 디스크립터도 GDT에 들어가며, init.inc에서 보듯이 TSS 디스크립터도 8바이트이므로 셀렉터 값이 8씩 건너뛰며 지정되고 있다.
Type은 1 0 B 1 4비트로 나누어져 있으며 1과 0으로 고정된 부분과 B 비트가 있다. B 비트는 이 태스크가 실행 중 혹은 실행을 기다리고 있는 중이라는 표시이다. CPU는 이 비트를 사용하여 실행 중 인터럽트가 걸린 태스크를 다시 CALL 했는지 아닌지를 검사한다. 처음에는 0으로 클리어 해둔다.

태스크 스위칭이 일어나는 순서(가장 중요)
1. 프로그램이 Protected Mode로 넘어오고, LTR 명령으로 TSS 영역을 지정한 후 jmp TSS2Selector:0 명령이 내려진다.
2. CPU 내부의 TR 레지스터를 참조하고 GDTR 레지스터를 참조하여 GDT에 있는 TSS1Selector(0x20) - 5번째의 디스크립터를 찾는다. (그 이유는 위 ltr 명령어를 통해 TSS1Selector 값(0x20)이 TR 레지스터에 들어가 있기 때문)
3. Tss1Selector 디스크립터의 Base Address를 참조하여 tss1 영역을 찾는다.
4. tss1 영역에 현재 CPU가 가지고 있는 모든 레지스터 값을 각각의 자리에 저장한다.
5. GDT에서 TSS2Selector(0x28) - 6번째 디스크립터를 찾는다. (그 이유는 순서 1번의 jmp TSS2Selector:0의 명령으로 TSS2 태스크 명령어의 실행을 위해서다.) 이때 TR 레지스터에는 TSS2Selector(0x28)이 들어가게 된다.
6. TSS2Selector 디스크립터에 있는 Base Address를 참조하여 tss2 영역을 찾는다.
7. tss2에 있는 모든 레지스터 값을 CPU에 복원한다. (이미 ESP에는 process2의 루틴 주소가 들어가 있음)
8. 복원이 되었을 때 ESP 레지스터에는 process2: 루틴의 주소가 있으므로 여기서부터 프로그램이 시작된다. EIP에 다른 값이 있다면 그 주소부터 프로그램이 시작될 것이다.


태스크 스위칭에 대해 반드시 이해하고 넘어가자..

인터럽트와 예외(예외)

예외의 종류는 아래와 같다.

프로그래머가 지정할 수 있는 인터럽트는 32번부터 255번까지이다.
이전에 PIC에 대해 알아보면서 PIC에 인터럽트가 걸렸을 때 CPU에게 IRQ 번호를 준다고 설명하였다.
PIC을 프로그램하지 않은 상태라면 IRQ는 0부터 시작하므로 이 상태로 만약 0번 인터럽트가 발생한다면 Protected Mde에서는 물론 IDT의 0번째 디스크립터를 참조하여 해당 인터럽트 핸들러를 실행시키므로 핸들러에서는 이것이 하드웨어 인터럽트인지, 예외가 발생한 것인지 모르게 되는 상태가 된다. 이러한 원인은 CPU에 지정된 예외와 PC 메인보드에 PIC이 연결된 방법이 처음부터 충돌이 나도록 디자인 되어 있기 때문이다. 이렇게 사용하는 것은 곤란하므로 하드웨어 인터럽트를 CPU에 지정된 유저 정의 인터럽트 쪽으로 모두 넘겨버리기 위해 PIC을 리맵핑 해주는 것이다.

예외를 발생시키는 프로그램을 작성해 보자.

init.inc 파일과 하단의 2개 파일 사용

boot.asm

위 파일은 섹터 변경된 것이다. 
    변경 내용 : mov al, 1    -> mov al, 2    ; 2섹터를 읽을 것이다. kernel 맨 마지막 times 1024 로 지정해 주
               었는데, 그 이유는 컴파일 도중 512바이트가 넘었다는 에러가 나기 때문
이다.

kernel.asm

%include "init.inc"


[org 0x10000]

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

lea esp, [PM_Start]

mov edi, 0

lea esi, [msgPMode]

call printf

cld

mov ax, SysDataSelector

mov es, ax

xor eax, eax

xor ecx, ecx

mov ax, 256 ; IDT 영역에 256개의

mov edi, 0 ; 디스크립터를 복사한다.

loop_idt:

lea esi, [idt_ignore]

mov cx, 8 ; 디스크립터 하나는 8바이트 이다.

rep movsb

dec ax

jnz loop_idt

; 먼저 디스크립터를 IDT에 포함시킨다. 0으로 나누는 예외는 IRQ 0번이므로 IDT의 맨 처음 부분에 디스크립터  를 포함시킨다.

mov edi, 0     ; IDT 덮어 쓴다.

lea esi, [idt_zero_divide]

mov cx, 8 ; 디스크립터 하나는 8바이트 이다.

rep movsb

mov edi, 8*0x20

lea esi, [idt_timer]

mov cx, 8

rep movsb

mov edi, 8*0x21 ; 키보드 IDT 디스크립터를 복사한다.

lea esi, [idt_keyboard]

mov cx, 8

rep movsb

lidt [idtr]

mov al, 0xFC ; 막아두었던 인터럽트 중

out 0x21, al ; 타이머와 키보드만 다시 유효하게 한다.

sti


mov edx, 0

mov eax, 0x100

mov ebx, 0

div ebx ; 인터럽트 발생

jmp $


printf:

push eax

push es

mov ax, VideoSelector

mov es, ax

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop es

pop eax

ret


msgPMode db "We are in Protected Mode", 0

msg_isr_ignore db "This is an ignorable interrupt", 0

msg_isr_32_timer db ".This is the timer interrupt", 0

msg_isr_33_keyboard db ".This is the keyboard interrupt", 0

msg_isr_zero_divide db "Zero Devide Exception!", 0


idtr:

dw 256*8-1 ; IDT의 Limit

dd 0 ; IDT의 Base Address


isr_ignore:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*7*2)

lea esi, [msg_isr_ignore]

call printf

popfd

popad

pop ds

pop es

pop fs

pop gs

iret

isr_zero_divide:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*6*2)

lea esi, [msg_isr_zero_divide]

call printf

jmp $    ; 여기서 무한루프가 걸려 더이상 진행이 되지 않는다. 타이머 인터럽트, 키보드 핸들러가 작동 안하
               는 이유이다.

popfd

popad

pop ds

pop es

pop fs

pop gs

iret

isr_32_timer:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*2*2)

lea esi, [msg_isr_32_timer]

call printf

inc byte [msg_isr_32_timer]

popfd

popad

pop ds

pop es

pop fs

pop gs

iret ; 인터럽트가 발생한 당시의 프로그램의 다음 명령으로 돌아가서 프로그램 재개


isr_33_keyboard:

pushad

push gs

push fs

push es

push ds

pushfd

in al, 0x60

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*4*2)

lea esi, [msg_isr_33_keyboard]

call printf

inc byte [msg_isr_33_keyboard]

popfd

pop ds

pop es

pop fs

pop gs

popad

iret

idt_ignore:

dw isr_ignore

dw 0x08

db 0

db 0x8E

dw 0x0001

idt_zero_divide:

dw isr_zero_divide

dw 0x08

db 0

db 0x8E

dw 0x0001

idt_timer:

dw isr_32_timer

dw 0x08

db 0

db 0x8E

db 0x0001


idt_keyboard:

dw isr_33_keyboard

dw 0x08

db 0

db 0x8E

dw 0x0001

times 1024-($-$$) db 0


위와 같이 소스를 짠 후 컴파일 및 실행 시 아래 그림과 같이 exception 이 발생된 것을 확인할 수 있다.


devide -> divide 로 알아봐주길.. 오타임..


위 내용은 0으로 나누었을 때 발생하는 예외를 구현한 것이다.

인터럽트와 예외(키보드 인터럽트 핸들러 구현)

아래와 같이 소스를 짠다. (파일은 이전 장에서 사용하던 파일 중 kernel 만 변경)

kernel.asm

책에서는 mov al, 0xFC 로 타이머와 키보드만 유효하게 하나, 실행되지 않으므로 0xFD 키보드만 유효하게 한다.

%include "init.inc"


[org 0x10000]

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

lea esp, [PM_Start]

mov edi, 0

lea esi, [msgPMode]

call printf

cld

mov ax, SysDataSelector

mov es, ax

xor eax, eax

xor ecx, ecx

mov ax, 256 ; IDT 영역에 256개의

mov edi, 0 ; 디스크립터를 복사한다.

loop_idt:

lea esi, [idt_ignore]

mov cx, 8 ; 디스크립터 하나는 8바이트 이다.

rep movsb

dec ax

jnz loop_idt

mov edi, 8*0x20

lea esi, [idt_timer]

mov cx, 8

rep movsb

mov edi, 8*0x21 ; 키보드 IDT 디스크립터를 복사한다.

lea esi, [idt_keyboard]    ; idt_keyboard는 디스크립터의 포인터 이다.

mov cx, 8

rep movsb

lidt [idtr]

mov al, 0xFD ; 막아두었던 인터럽트 중

out 0x21, al ; 타이머와 키보드만 다시 유효하게 한다.

sti


jmp $


printf:

push eax

push es

mov ax, VideoSelector

mov es, ax

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop es

pop eax

ret


msgPMode db "We are in Protected Mode", 0

msg_isr_ignore db "This is an ignorable interrupt", 0

msg_isr_32_timer db ".This is the timer interrupt", 0

msg_isr_33_keyboard db ".This is the keyboard interrupt", 0


idtr:

dw 256*8-1 ; IDT의 Limit

dd 0 ; IDT의 Base Address


isr_ignore:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*7*2)

lea esi, [msg_isr_ignore]

call printf

popfd

popad

pop ds

pop es

pop fs

pop gs

iret

isr_32_timer:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*2*2)

lea esi, [msg_isr_32_timer]

call printf

inc byte [msg_isr_32_timer]

popfd

popad

pop ds

pop es

pop fs

pop gs

iret ; 인터럽트가 발생한 당시의 프로그램의 다음 명령으로 돌아가서 프로그램 재개


isr_33_keyboard:    ; 인터럽트 핸들러 루틴

pushad

push gs

push fs

push es

push ds

pushfd

in al, 0x60    ; 키보드에서 어느 키가 눌렸는지 알아내기 위해 사용하는 명령어, 키보드 버퍼에 있는 문자 스
                    캔코드를 가져온다.
 이 스캔코드는 아스키코드와는 다르다. 또한 버퍼에서 문자를 가져오지 않
                    으면 키보드에서 인터럽트는 발생하지 않는다.

; PIC 리셋 코드

mov al, 0x20    ; 마스터 PIC는 0x20, 0x21 포트를 사용하고, 슬레이브 PIC는 0xA0, 0xA1 포트를 사용한다.

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*4*2)

lea esi, [msg_isr_33_keyboard]

call printf

inc byte [msg_isr_33_keyboard]    ; 아스키코드 값이 1씩 추가된다. 여기서 맨 처음 출력되는 것은 '.' 이고
                                        엔터 시 값이 아스키 값이 1씩 증가되어 문자열이 출력된다.

popfd

pop ds

pop es

pop fs

pop gs

popad

iret

idt_ignore:

dw isr_ignore

dw 0x08

db 0

db 0x8E

dw 0x0001

idt_timer:

dw isr_32_timer

dw 0x08

db 0

db 0x8E

db 0x0001


idt_keyboard:

dw isr_33_keyboard

dw 0x08

db 0

db 0x8E

dw 0x0001

times 512-($-$$) db 0

컴파일 후 실행 시 키보드를 누를때, 뗄때마다 맨 앞의 값이 하나 바뀌는것을 알 수 있다.


신기방기 하구만.. 하하..








인터럽트와 예외(하드웨어 인터럽트)

아래와 같이 소스를 짠다. (파일은 올려둠)


boot.asm


init.inc


kernel.asm


init.inc 는 이전장에서와 동일한 파일을 쓴다.

boot.asm

%include "init.inc"


[org 0]

jmp 07C0h:start

start:

mov ax, cs

mov ds, ax

mov es, ax

reset: ; 플로피 디스크를 리셋한다.

mov ax, 0

mov dl, 0 ; Drive = 0 (A:)

int 13h

jc reset ; 에러가 나면 다시 한다.

mov ax, 0xB800

mov es, ax

mov di, 0

mov ax, word [msgBack]

mov cx, 0x7FF

paint:

mov word [es:di], ax

add di, 2

dec cx

jnz paint

read:

mov ax, 0x1000 ; ES:BX = 1000:1000

mov es, ax

mov bx, 0

mov ah, 2 ; 디스크에 있는 데이터를 es:bx의 주소로

mov al, 1 ; 1섹터를 읽을 것이다.

mov ch, 0 ; 0번째 Cylinder

mov cl, 2 ; 2번째 섹터부터 읽기 시작

mov dh, 0 ; Head = 0

mov dl, 0 ; Drive=0, A: 드라이브

int 13h ; read

jc read ; 에러가 나면 다시함

mov dx, 0x3F2 ; 플로피디스크 드라이브의

xor al, al ; 모터를 끈다.

out dx, al

; 아래 5개 루틴은 ICW1을 마스터 PIC과 슬레이브 PIC에 프로그램 한다.

mov al, 0x11 ; PIC의 초기화    -> 4번 비트 1, 0번 비트(IC4)에 1이 있다. 이것은 PIC 초기화 명령 +
                                          ICW4 명령이 필요하다는 뜻이다.

out 0x20, al ; 마스터 PIC

dw 0x00eb, 0x00eb ; jmp $+2, jmp $+2    -> 하나의 명령을 넣고 약간의 시간 딜레이를 주기 위해 기계
                                                   어로 바꿈, 16비트 리얼모드 명령이므로 $+2로 현재 주소
                                                   에서 2바이트 뒤를 가리키면 다음 명령의 앞을 지칭
, 즉 첫
                                                   번째 0x00eb는 두번째 0x00eb로 점프 , 두번째 0x00eb는
                                                    다음 명령어인 out 0xA0, al 로 점프한다.

out 0xA0, al ; 슬레이브 PIC

dw 0x00eb, 0x00eb


; 아래 6개 루틴은 ICW2 명령

mov al, 0x20 ; 마스터 PIC 인터럽트 시작점(마스터 PIC는 0x20부터 시작), 만약 IRQ 0에 연결된 하
                            드웨어에서 인터럽트 발생 시 PIC이 CPU에 알려주는 IRQ 번호는 0x20, 1일 때 0x21
                            이 된다.

out 0x21, al

dw 0x00eb, 0x00eb

mov al, 0x28 ; 슬레이브 PIC 인터럽트 시작점(슬레이브 PIC는 0x28부터 시작),만약 IRQ 0에 연결된
                            하
드웨어에서 인터럽트 발생 시 PIC이 CPU에 알려주는 IRQ 번호는 0x28, 1일 때
                            0x29
이 된다. 이렇게 IRQ 번호를 바꾸는 것을 다시 매핑한다 하여 리매핑이라고 함

out 0xA1, al

dw 0x00eb, 0x00eb

; ICW3 명령어

mov al, 0x04 ; 마스터 PIC에 IRQ 2번에(마스터 PIC의 0, 1, 2 번째 비트 즉 3번째)

out 0x21, al ; 슬레이브 PIC이 연결되어 있다. (마스터 PIC에 알리는 과정)

dw 0x00eb, 0x00eb

mov al, 0x02 ; 슬레이브 PIC이 마스터 PIC의

out 0xA1, al ; IRQ 2번에 연결되어 있다.(슬레이브 PIC에 알리는 과정)

dw 0x00eb, 0x00eb

; ICW4 명령어

mov al, 0x01 ; 8086 모드를 사용한다.

out 0x21, al

dw 0x00eb, 0x00eb

out 0xA1, al

dw 0x00eb, 0x00eb

; 모든 인터럽트를 막아놓는 부분

mov al, 0xFF

out 0xA1, al

dw 0x00eb, 0x00eb

mov al, 0xFB

out 0x21, al

-> Protected Mode로 넘어가면서 IDT로 설정해야 하고, 하드웨어 인터럽트 관련으로 해야 할 몇가지 일들이 존재한다. 그런 일을 할 동안 하드웨어 인터럽트가 걸린다면 프로그램이 제대로 작동하지 않기 때문에 막아놓는것이 편하다.


lgdt[gdtr]

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

jmp $+2

nop

nop

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

jmp dword SysCodeSelector:0x10000

msgBack db '.', 0x67

gdtr:

dw gdt_end - gdt - 1 ; GDT의 Limit

dd gdt+0x7C00 ; GDT의 Base Address

gdt:

dd 0, 0

dd 0x0000FFFF, 0x00CF9A00

dd 0x0000FFFF, 0x00CF9200

dd 0x8000FFFF, 0x0040920B

gdt_end:


times 510-($-$$) db 0

dw 0AA55h


-----------------------------------------------------

kernel.asm

%include "init.inc"


[org 0x10000]

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

lea esp, [PM_Start]

mov edi, 0

lea esi, [msgPMode]

call printf

cld

mov ax, SysDataSelector

mov es, ax

xor eax, eax

xor ecx, ecx

mov ax, 256 ; IDT 영역에 256개의

mov edi, 0 ; 디스크립터를 복사한다.

loop_idt:

lea esi, [idt_ignore]

mov cx, 8 ; 디스크립터 하나는 8바이트 이다.

rep movsb

dec ax

jnz loop_idt

mov edi, 8*0x20    ; 타이머는 PIC의 IRQ 0번이다. 그러나 부트스트랩 프로그램에서 PIC을 리매핑 하였기 때
                         문에 타이머 인터럽트가 발생했을 때 PIC은 CPU에게 0x20(10진수 32)번의 인터럽트
                          발생했다고 알려줄 것이다. 그러므로 우리는 IDT에 있는 디스크립터 중 0x20번째에 만
                          들어 놓은 디스크립터를 복사
하여 설정해 놓는다.

lea esi, [idt_timer]

mov cx, 8

rep movsb

-> 위 루틴이 실행되면 그 자리에 있던 idt_ignore라는 이름의 디스크립터는 덮어 씌어지게 된다.


; idt_timer 디스크립터 이다.

lidt [idtr]

mov al, 0xFE ; 막아두었던 인터럽트 중(0xFE로써 0번째 비트 0으로 셋팅, 즉 타이머만 유효하게 됨)

out 0x21, al ; 타이머만 다시 유효하게 한다. (0x21은 마스터 PIC)

sti            ; sti도 해주어야 하는데 CPU가 PIC으로부터 인터럽트를 받아들이고, /INTA 신호를 되돌려 주기
                    위함
이다. cli 명령을 내려놓으면 CPU 측에서 /INTA 신호를 되돌려 주기 않게 된다. 그리고
                    jmp $ 명령으로 무한루프를 돌게하여 프로그램은 사실상 여기서 멈추게 된다. 이 후로 타이머
                    인터럽트 핸들러가 계속 호출되어 문장이 나온다. 하단 핸들러의 inc byte !~~ 부분으로 인해
                    서 값이 변경된다.


jmp $


printf:

push eax

push es

mov ax, VideoSelector

mov es, ax

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop es

pop eax

ret


msgPMode db "We are in Protected Mode", 0

msg_isr_ignore db "This is an ignorable interrupt", 0

msg_isr_32_timer db ".This is the timer interrupt", 0


idtr:

dw 256*8-1 ; IDT의 Limit

dd 0 ; IDT의 Base Address


isr_ignore:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*7*2)

lea esi, [msg_isr_ignore]

call printf

popfd

popad

pop ds

pop es

pop fs

pop gs

iret

isr_32_timer:

push gs

push fs

push es

push ds

pushad

pushfd

mov al, 0x20

out 0x20, al

mov ax, VideoSelector

mov es, ax

mov edi, (80*2*2)

lea esi, [msg_isr_32_timer]

call printf

inc byte [msg_isr_32_timer]

popfd

popad

pop ds

pop es

pop fs

pop gs

iret ; 인터럽트가 발생한 당시의 프로그램의 다음 명령으로 돌아가서 프로그램 재개

idt_ignore:

dw isr_ignore

dw 0x08

db 0

db 0x8E

dw 0x0001

idt_timer:

dw isr_32_timer    -> 핸들러의 물리주소로써 0x10000(최 상단 org 참조) + isr_32_timer 를 지정하고 있다.

dw 0x08

db 0

db 0x8E

db 0x0001

times 512-($-$$) db 0

------------------------------------------------------------

컴파일 후 img 파일을 만든다.

해당 img를 가지고 부팅을 시켜볼 시 2번째 줄 첫번째가 꾸준히 변하는것을 확인할 수 있다.

첫번째가 꾸준히 변하고 있음.. 동작 원리에 대해 이해하기 위해서는 아래를 이해하여야 한다.

PC의 모든 외부로부터의 하드웨어 인터럽트는 8259A 라는 칩을 통하여 입력을 받는다. 이 8258A는 보통 PIC 라고 지칭한다.
이 PIC도 하나의 컨트롤러 개념을 가지고 있어서 조그마한 프로그램을 넣어 조작할 수 있다. 이 프로그램에는 초기화, 여러 개의 PIC 연결방법, 인터럽트를 받아들이는 방법, 받아들인 인터럽트에 대해 CPU에게 알려주는 방법 등이 기재되어야 한다.
우리가 사용하는 PC에서 PIC는 아래 그림과 같이 연결되어 있다. 하나의 PIC은 8개의 IRQ 핀을 가지고 있다.

PIC 두개가 마스터, 슬레이브로 연결되어 있고, 마스터의 INT핀이 CPU의 INT핀으로 연결되어 있다. 슬레이브의 INT핀마스터의 3번째 IRQ(Interrupt Request line) 핀인 2번 핀에 연결되어 있다. 그리고 두 PIC 모두 /INTA 핀이 CPU의 INTA와 연결되어 있다. 각 IRQ 핀에는 다음의 표와 같이 여러 장치들의 인터럽트 선이 각각 연결되어 있다.

마스터 PIC의 동작원리는 아래 순서대로 진행된다.
    1. 마스터 PIC에 연결된 장치 중 하나에서 인터럽트가 발생하면
    2. 마스터 PIC은 자신의 INT핀에 신호를 실어 CPU의 INT핀에 신호를 준다.
    3. CPU는 이것을 받고 EFLAG 레지스터의 IE비트가 1로 세트되어 인터럽트를 받을 수 있는 상황이라면 ?INTA를
      통해 마스터 PIC에 인터럽트를 잘 받았다는 신호를 보낸다.
    4. 마스터 PIC은 /INTA 신호를 받으면 몇 번째 IRQ에서 연결된 장치에서 인터럽트가 발생했는지를 숫자로 데이터
      버스를 통해 CPU로 전달한다.
    5. CPU는 이 데이터를 참조하여 Protected Mode로 실행중이라면 IDT에서 그 번호에 맞는 디스크립터를 찾아 아
      래와 같은 순서를 진행한다.

        (1) 0x20번의 인터럽트가 발생한다.
        (2) CPU는 IDTR을 참조한다.
        (3) IDTR은 IDT의 Base Address를 가지고 있으므로 IDT의 첫 번째 번지 부분을 가리키게 된다.
        (4) 인터럽트 번호가 0x20이므로 IDT의 첫번째 번지에서 0x20번째의 디스크립터를 찾아낸다.
        (5) IDT는 GDT와 달리 테이블의 맨 처음 디스크립터도 사용하므로, 다른 예로 인터럽트 번호 0번이 발생하
           면 맨
 처음 디스크립터를 찾아내게 된다.
        (6) 디스크립터(IDT에 위치한)에는 핸들러가 위치한 세그먼트의 세그먼트 셀렉터와 오프셋이 있다. 이 중에
          서 먼저 세그먼트 셀렉터 값을 가지고 GDT에서 해당하는 디스크립터를 찾아낸다.
        (7) GDT의 디스크립터를 가지고 RAM상의 해당 세그먼트(여기에서는 커널 코드 세그먼트)의 Base Address
           를 찾아낸다. 우리가 작성한 프로그램에서 커널 코드 세그먼트의 Base Address가 물리 주소 0번지에서
           시작하므로 0번지가 된다.
        (8) IDT의 0x20번째 디스크립터에 포함된 오프셋 값을 가지고 인터럽트 핸들러 루틴이 세그먼트 범위 안에서
          실제로 위치한 곳을 찾아낸다.
        (9) 핸들러가 모두 실행되어 iret 명령이 내려지면 처음 인터럽트 걸렸던 명령문 다음 명령으로 돌아간다.

슬레이브 PIC의 동작원리는 아래 순서대로 진행된다.
    1. 슬레이브 PIC에 연결된 장치 중 하나에서 인터럽트가 발생하면
    2. 슬레이브 PIC은 자신의 INT 핀에 신호를 실어 마스터 PIC의 IRQ 2번 핀에 인터럽트 신호를 보낸다.
    3. 마스터 PIC은 자신의 IRQ핀에서 인터럽트가 발생하였으므로 자신의 INT 핀에 신호를 실어 CPU에게 알린다.
    4. CPU가 /INTA 신호를 주면 역시 데이터 베이스에 숫자를 실어 CPU에게 몇 번째 IRQ에서 인터럽트가 발생했는
      지를 알려준다. 이 경우 숫자는 8 ~ 15 사이가 될 것이다.

마스터 PIC과 슬레이브 PIC이 제대로 동작하도록 하기 위해서는 이 PIC들을 초기화시켜 줄 필요가 있다. 마스터 PIC도, 슬레이브 PIC도 자신이 마스터에 해당하는지, 슬레이브에 해당하는지를 알아야 하고, 어떤 모드로 움직일지 등등을 각 PIC에 프로그램 해주어야 한다.

이 프로그램은 ICW1, ICW2, ICW3, ICW4 총 4가지로 구성되어 있다. ICW는 하나의 명령어라고 보면 된다.
프로그램은 ICW1, ICW2, ICW3, ICW4의 순서로 이루어 진다.

ICW의 명령어

ICW1은 PIC을 초기화 하는 명령어이다.

7 ~ 4 비트는 정해진 것이다.
LTIM은 인터럽트가 발생할 때 그 인터럽트 신호의 엣지에서 인터럽트 발생을 인정할 것인지, 혹은 HIGH Level로 신호가 모두 올라온 상태에서 인터럽트를 발생을 인정할 것인지를 나타낸다. 0 이면 엣지 트리거링, 1이면 레벨 트리거링 이다.
SNGL은 이 PIC이 마스터/슬레이브로 구성되어 있는지, 마스터 하나만 사용할지를 나타낸다. 0이면 마스터/슬레이브 형식으로 PIC을 2개 사용, 1이면 마스터 하나만 사용 한다는 뜻이다.
IC4는 ICW4 명령어가 추가적으로 필요한지를 나타낸다. 0 이면 필요하지 않다는 뜻이고, 1 이면 필요하다는 뜻이다.

ICW2는 이 PIC이 인터럽트를 받았을 때 IRQ 번호에 얼마를 더해서 CPU에게 알려줄지를 지정한다.

0 ~ 2 비트가 0인 것은 이 숫자를 8 단위로 기재해야 한다는 뜻 이다.
예를 들어 2진수로 00010000(16진수 0x10)을 넣으면 나중에 인터럽트 0번이 발생했을 때 CPU에게는 0x10의 숫자를 보내준다. IRQ 16번이라고 알려주는 것이다. 인터럽트 1번이 발생했을 때에는 CPU에게 0x11의 숫자를 보내준다.
2진수로 00100000(16진수 0x20)을 넣으면 나중에 인터럽트 0번이 발생했을 때 CPU에게는 0x20의 숫자를 보내준다. 인터럽트 1번이 발생했을때 CPU에게 0x21의 숫자를 보내준다.
일부러 CPU에게 다른 수를 보내주도록 하는 것처럼 보이지만 CPU에 설정된 exception(예외) 번호와 PC의 메인보드에서의 인터럽트 관련 회로 구현에서 충돌이 일어날 수 있기 때문에 하드웨어 인터럽트 번호를 바꾸어 줄 필요가 있다.

ICW3은 각 PIC의 마스터, 슬레이브로서의 연결 방법을 나타낸다.

S0 ~ S7은 마스터 PIC의 각 IRQ 선에 해당된다.
각 비트에 0을 넣으면 그 IRQ 선은 하드웨어 장치에 연결되어 있다는 것을 의미한다.
각 비트중 어느 비트에 1을 넣으면 그 IRQ 선은 슬레이브 PIC에 연결되어 있다는 것을 나타낸다.

3 ~ 7 비트는 0으로 해준다.
ID0 ~ ID2의 3비트를 사용하여 슬레이브 PIC이 마스터 PIC의 몇 번째 IRQ 핀에 연결되어 있는지를 마스터 PIC에서 해당 비트를 1로 세트하는 ICW3과는 달리 여기서는 숫자로 나타낸다.

ICW4는 추가 명령어이다.

SFNM, BUF, M/S의 기능은 현재 우리가 사용하는 PC에는 구현되지 않아도 되므로 0 으로 해둔다.
AEOI 비트는 PIC의 Reset을 자동으로 할 것인지, 수동으로 할 것인지 나타낸다.
UPM 비트에 0을 넣으면 이 PIC이 MCS-80/85 모드로 움직인다는 뜻이고, 1을 넣으면 이 PIC이 8086 모드로 움직인다는 것을 나타낸다. 우리 PC는 8086 계열이므로 1을 넣는다.

PIC에 프로그램 할 때 I/O 명령어 out을 사용하여 마스터 PIC은 I/O 주소 0x20과 0x21에, 슬레이브 PIC은 I/O 주소 0xA0, 0xA1에 프로그램 한다.





인터럽트와 예외

아래와 같이 소스를 짠다. (파일은 올려둠)


boot.asm


init.inc


kernel.asm


init.inc


SysCodeSelector equ 0x08

SysDataSelector equ 0x10

VideoSelector equ 0x18


boot.asm


%include "init.inc"


[org 0]

jmp 07C0h:start

start:

mov ax, cs

mov ds, ax

mov es, ax

mov ax, 0xB800

mov es, ax

mov di, 0

mov ax, word [msgBack]

mov cx, 0x7FF

paint:

mov word [es:di], ax

add di, 2

dec cx

jnz paint

read:

mov ax, 0x1000

mov es, ax

mov bx, 0

mov ah, 2

mov al, 1

mov ch, 0

mov cl, 2

mov dh, 0

mov dl, 0

int 13h

jc read

mov dx, 0x3F2    ; 플로피 디스크 드라이브의 모터를 끄는 루틴

xor al, al

out dx, al    ; 0x3F2번지에 I/O 명령어인 out으로 0을 넣게 되면 모터가 멈추게 된다.

cli

mov al, 0xFF

out 0xA1, al

lgdt[gdtr]

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

jmp $+2

nop

nop

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

jmp dword SysCodeSelector:0x10000

msgBack db '.', 0x67

gdtr:

dw gdt_end - gdt - 1

dd gdt+0x7C00

gdt:

dd 0, 0

dd 0x0000FFFF, 0x00CF9A00

dd 0x0000FFFF, 0x00CF9200

dd 0x0000FFFF, 0x0040920B

gdt_end:


times 510-($-$$) db 0


dw 0AA55h


kernel.asm

%include "init.inc"


[org 0x10000]

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

lea esp, [PM_Start]    ; 스택 초기화 부분
                             이 명령문의 바로 전까지 값은 PC가 부팅한 이후 사용한 값이 16비트 Real Mode인
                            boot.asm을 거치는 동안 계속 가지고 있던 값이며, PC의 BIOS마다 조금씩 다르다.
                            Protected Mode에서 이렇게 초기화 해주지 않을 시 PUSH, POP으로 스택 사용
                           시 
프로그램을 건드리게 되어 에러가 발생할 가능성이 있다. 그러므로 명시적으로 번
                           지수를 바꿔주어 관리를 하는 것이 안전하다.

mov edi, 0

lea esi, [msgPMode]

call printf

cld

mov ax, SysDataSelector    ; SysDataSelector를 ES 레지스터에 선택하게 함

mov es, ax

xor eax, eax    ; 초기화

xor ecx, ecx    ; 초기화

mov ax, 256 ; IDT 영역에 256개의

mov edi, 0 ; 디스크립터를 복사한다.

loop_idt:

lea esi, [idt_ignore]    ; 디스크립터의 샘플이 있는 주소를 넣는다.

mov cx, 8    ; 디스크립터 하나는 8 바이트이다.

rep movsb    ; CX에 8이 들어있으므로 8바이트가 DS:ESI -> ES:EDI의 방향으로 복사됨

dec ax    ; ax를 하나 줄인다.

jnz loop_idt

lidt [idtr]

sti    ; 인터럽트를 활성화 시킨다. 이 명령을 내리면 EFLAGS의 IE비트가 세트되어 CPU가 이후 명령부터 인
            터럽트를 받아 들이게 된다.

int 0x77    ; 소프트웨어 인터럽트를 발생시키는 명령, 이 명령어 발생 시 하단 중요사항 정리 4번과 같은 과
                  정
이 발생한다.

jmp $

; Subroutines

printf:

push eax

push es

mov ax, VideoSelector

mov es, ax

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop es

pop eax

ret

; Data Area

msgPMode db "We are in Protected Mode", 0

msg_isr_ignore db "This is an ignorable interrupt", 0

msg_isr_32_timer db ".This is the timer interrupt", 0


; Interrupt Service Routines

isr_ignore:    ; 인터럽트 발생 시 실행되어야 할 핸들러 루틴

push gs

push fs

push es

push ds

pushad

pushfd

; 인터럽트 발생 시 먼저 CPU의 모든 레지스터 값과 FLAG를 스택에 보존해야 한다.

; 루틴이 끝나면 다시 원래대로 돌림

mov ax, VideoSelector

mov es, ax

mov edi, (80*7*2)

lea esi, [msg_isr_ignore]

call printf

popfd

popad

pop ds

pop es

pop fs

pop gs

iret    ; 다음 명령으로 돌아가서 프로그램 재개

; IDT

idtr:

dw 256*8-1 ; IDT의 Limit

dd 0 ; IDT의 Base Address

idt_ignore:

dw isr_ignore    ; isr_ignore의 번지수를 기입하고 맨 아래 줄에 1을 추가

dw SysCodeSelector    ; 코드 세그먼트 셀렉터는 SysCodeSelector 기입

db 0

db 0x8E

dw 0x0001

times 512-($-$$) db 0


컴파일 후 부트 시 플로피 디스크 드라이브의 모터를 끄는 루틴이 추가되어 LED가 꺼지는 것을 알 수 있다. (VM 상에서 실험하여 실제로는 확인 불가)


중요사항 정리

1. IDT(Interrupt Descriptor Table)는 Protected Mode 내 인터럽트를 구현하기 위해 필요한 테이블이다.

2. IDT는 RAM에 저장되며 메모리 어디든 저장 가능, 256개의 디스크립터로 구성되어 있음


처음 16비트 한 워드에는 인터럽트 핸들러가 자리하고 있는 RAM 상의 물리주소의 0~15비트(오프셋)을 기입
2번째 16비트 한 워드에는 인터럽트 핸들러가 자리하고 있는 코드 세그먼트 셀렉터 값 기입, 인터럽트 핸들러는
항상 Protected Mode상에서 동작하므로 커널 모드 코드 세그먼트 셀렉터 값을 기입하면 된다.
그후 한 바이트는 여러 비트로 나뉘어져 있으며, P 비트는 GDT 디스크립터와 같이 P비트이며 1로 셋팅
DPL은 핸들러가 실행될 특권 레벨 지정, 인터럽트 핸들러는 항상 커널모드에서 동작하기 때문에 00을 기입
0과 1로 정해진 비트 값들은 이 디스크립터가 IDT에 위치한 인터럽트 관련 디스크립터라는 것을 알려주는 값(그대로 나두면 됨)
D비트는 현재 지정한 코드 세그먼트가 16비트인지 32비트인지 나타냄, 인터럽트 핸들러는 32비트이므로 1 설정
나머지 상위 16비트의 한 워드는 핸들러의 오프셋의 상위 16비트를 기입하게 되어있음. 핸들러의 오프셋은 상위, 하위로 나누어 기입한다는 것을 알 수 있다.
이 형태의 디스크립터를 메모리상에 256개를 만들어 하나의 IDT의 형태를 갖추게 함
256개보다 많거나 적어도 상관 없지만, 메인보드의 인터럽트 관련 하드웨어가 256개의 인터럽트를 받아들이도록 디자인 되어 있기 때문에 대부분 256개로 한다.

3. Protected Mode에서 인터럽트를 구현하기 위해서 실제로 IDT를 RAM 상에 작성하는 루틴, 디스크립터 샘플, 인터럽트 핸들러 루틴(Interrupt Service Routine, 약자로 ISR이라고 부름), 이렇게 3가지가 필요.
이렇게 만들어 놓은 IDT도 GDT와 마찬가지로 CPU에 인터럽트가 걸렸을 때 CPU로 하여금 참조 가능하도록 등록하여야 한다.
그 부분이 바로 아래와 같이 IDT를 CPU의 IDTR 레지스터에 등록 시켜주는 명령어이다.
lidt [idtr]
idtr 주소 포인터 이후의 데이터는 아래와 같이 정의되어 있음
idtr:
    dw 256 * 8 - 1
    dd 0
IDT 등록을 위해 48비트 변수가 필요하며, 이것은 현재 IDT의 크기를 나타내는 16비트 워드 값, IDT의 시작 주소를 나타내는 하나의 32비트 더블워드 형태로 구성되어 있다.

4. 인터럽트 걸린 후 핸들러가 호출되기 까지의 과정은 아래와 같다.
    (1) 0x20번의 인터럽트가 발생한다.
    (2) CPU는 IDTR을 참조한다.
    (3) IDTR은 IDT의 Base Address를 가지고 있으므로 IDT의 첫 번째 번지 부분을 가리키게 된다.
    (4) 인터럽트 번호가 0x20이므로 IDT의 첫번째 번지에서 0x20번째의 디스크립터를 찾아낸다.
    (5) IDT는 GDT와 달리 테이블의 맨 처음 디스크립터도 사용하므로, 다른 예로 인터럽트 번호 0번이 발생하면 맨
        처음 디스크립터를 찾아내게 된다.
    (6) 디스크립터(IDT에 위치한)에는 핸들러가 위치한 세그먼트의 세그먼트 셀렉터와 오프셋이 있다. 이 중에서
        먼저 세그먼트 셀렉터 값을 가지고 GDT에서 해당하는 디스크립터를 찾아낸다.
    (7) GDT의 디스크립터를 가지고 RAM상의 해당 세그먼트(여기에서는 커널 코드 세그먼트)의 Base Address를
        찾아낸다. 우리가 작성한 프로그램에서 커널 코드 세그먼트의 Base Address가 물리 주소 0번지에서 시작
       하므로 0번지가 된다.
    (8) IDT의 0x20번째 디스크립터에 포함된 오프셋 값을 가지고 인터럽트 핸들러 루틴이 세그먼트 범위 안에서 실
        제로 위치한 곳을 찾아낸다.
    (9) 핸들러가 모두 실행되어 iret 명령이 내려지면 처음 인터럽트 걸렸던 명령문 다음 명령으로 돌아간다.


다음에는 하드웨어 인터럽트 동작을 체험해 보자..




Protected Mode로 변환하기

2장에서 사용한 boot.txt 파일을 그대로 사용하며 kernel.txt 파일만 아래와 같이 작성한다.

[org 0]

[bits 16]


start:

mov ax, cs

mov ds, ax

xor ax, ax

mov ss, ax

cli

lgdt[gdtr]    ->  lgdt[gdtr] 명령어를 통해 GDT를 등록시킴.


mov eax, cr0

or eax, 0x00000001

mov cr0, eax

-> CPU에는 CR0, CR1, CR2, CR3 레지스터가 있으며, 프로그래머가 디버깅 할 때 사용
    위 3줄은 CR0에 0x00000001을 OR 연산을 통해 추가하는 루틴
    CR0의 최하위 비트는 PE비트라 하여 이제부터 Protected Mode이다 라는 것을 표시
    이 비트가 세트된 후 Protected Mode 안에서 동작하게 된다.
    OR 연산을 이용한 이유는 CR0 레지스터에 다른 비트 값들이 손상되지 않도록 하기 위해서이다.
   
위 루틴을 통해 Protected Mode로 바뀌나, CPU 내부 사정 상 한가지 추가작업을 해주어야 한다.
   
CPU에는 명령을 읽고, 해석하고 실행하는 각각의 유닛이 있다.
    이 동작은 동시에 진행되며 CR0 최하위 비트를 세트하는 명령 실행 동안 그 다음 명령이 해석 중에
   있고 그 다음 다음 명령이 읽혀지고 있다.


jmp $+2

nop

nop


db 0x66    -> Operand Prefix라고 하여 CPU에게 16비트 명령이 32비트 명령으로 바뀌었다 라던가 32비트
                    명령이 16비트로 바뀌었다는 것을 알려주기 위한 표시, 0x66은 32비트 및 16비트 명령어 사용
                    16비트에서 0x66 사용 시 32비트로 전환, 32비트에서 0x66 사용 시 16비트로 전환

db 0x67    -> 16비트 코드가 진행중이지만 32비트 주소값을 사용, 66 사용 및 67 사용 시 그 뒤로부터 자동으
                    로 자동으로 32비트로 사용됨

db 0xEA    -> JMP 명령을 그대로 기계어로 바꿔놓은 것

dd PM_Start

dw SysCodeSelector


;------------------------------------------------------------------;

;************** 여기부터 Protected Mode 입니다. *******************;

;------------------------------------------------------------------;


[bits 32]


PM_Start:    -> 셀렉터 레지스터에 16비트가 남아있기 때문에 초기화를 위한 작업 해주는 곳

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx


xor eax, eax

mov ax, VideoSelector

mov es, ax

mov edi, 80*2*10+2*10    -> 문자열의 위치 (한 열의 칸 수 * 2 * 써야할 열 번호 + 2 * 서야할 칸 번호)

lea esi, [ds:msgPMode]

call printf


jmp $


;------------------------------------------------------------------;

;************** Sub Routines        *******************;

;------------------------------------------------------------------;

printf:

push eax


printf_loop:

or al, al

jz printf_end

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

jmp printf_loop


printf_end:

pop eax

ret


msgPMode db "We are in Protected Mode", 0


;------------------------------------------------------------------;

;************** GDT Table 입니다. *******************;

;------------------------------------------------------------------;

gdtr:

dw gdt_end - gdt - 1 ; GDT의 limit

: 워드 값으로 GDT의 크기를 나타내며, 이 프로그램에서 사용하는 GDT에 포함되어 있는 모든 디스크립터가 차지하는 총 바이트 량

디스크립터는 NULL, SysCode, SysCodeData, Video 4개이며 한개가 8바이트 이므로, 총 바이트량은 8 * 4 = 32 바이트이다. 그러므로 GDTR 레지스터의 사용할 GDT의 크기 부분에 들어갈 숫자는 32이다.

gdt_end - gdt - 1의 표현을 보면 GDT의 마지막 번지에서 첫번째 번지를 빼고, 거기에서 1을 또 빼게 되어 있음. gdt_end는 GDT의 맨 끝 주소의 다음 주소를 가리키고 있기 때문에 gdt_end - gdt에서 1을 또 빼주어야 한다.

dd gdt+0x10000 ; GDT의 Base Address

GDT의 시작 번지를 물리 주소 값으로 가지고 있음. 그러나 주소값에 0x10000을 더하고 있다. 이 프로그램의 첫 부분에서 [org 0]으로 시작하였기 때문에 이 소스 안에 있는 모든 주소는 0을 기준으로 하고 있으며 프로그램의 물리 주소의 시작은 0x10000이고, gdt: 번지도 0x10000 이후에 있을 것이다.
GDTR에 넣어야 하는 값은 세그먼트:오프셋 이 아닌 물리주소 이므로 gdt: 번지가 실제 존재하는 물리 주소의 값을 만들어 GDTR에 넣어야 한다.


;NULL 디스크립터 ( GDT 첫번째는 형식 상 NULL 디스크립터를 기재해야함, 그렇지 않으면 실행 도중 에러 발생)

gdt:   

dw 0 ; limit 0~15비트

dw 0 ; Base Address의 하위 두 바이트

db 0 ; Base Address 16~23 비트

db 0 ; 타입

db 0 ; limit 16~19비트, 플래그

db 0 ; Base Address 31~24비트


; 코드 세그먼트 디스크립터

SysCodeSelector equ 0x08

dw 0xFFFF ; limit:0xFFFF

dw 0x0000 ; base 0~15 bit

db 0x01 ; base 16~23 bit

db 0x9A ; P:1, DPL:0, Code, non-conforming, readable

   -> 0x9(2진수 1001) 4번째 비트가 1, 이것은 코드 세그먼트를 의미

  DPL 은 00이므로 커널 영역으로 사용됨을 확인 가능

  첫번째 비트 1, 따라서 0xFFF를 곱해줘야함

  Limit 0xFFFFF(바로 아래 줄 Limit 16~19가 0xF이므로) * FFF = 0xFFFFFFFF


db 0xCF ; G:1, D:1, limit 16~19(bit:0xF)

D 비트가 1이므로 이 세그먼트는 32비트 코드를 담을 수 있는 세그먼트

db 0x00 ; base 24~32 bit


; 데이터 세그먼트 디스크립터

SysDataSelector equ 0x10

dw 0xFFFF ; limit:0xFFFF

dw 0x0000 ; base 0~15 bit

db 0x01 ; base 16~23 bit

db 0x92 ; P:1, DPL:0, Data, expand-up, writeable

db 0xCF ; G:1, D:1, limit 16~19 bit:0xF

db 0x00 ; base 24~32 bit


; 비디오 세그먼트 디스크립터

VideoSelector equ 0x18

dw 0xFFFF ; limit:0xFFFF

dw 0x8000 ; base 0~15 bit

db 0x0B ; base 16~23 bit    -> Base Address는 0xB8000이 된다.

db 0x92 ; P:1, DPL:0, Data, expand-up, writable

db 0x40 ; G:0, D:1, limit 16~19 bit:0x0

db 0x00 ; base 24~32 bit

gdt_end:


--------------------------------------------------------------------------


작성 완료 후 아래와 같이 컴파일 한다.


컴파일 후 img를 플로피에 넣어 결과를 확인한다.



Protected Mode에서 출력 성공!



오늘 중요사항 정리

1. Real Mode에는 16비트 레지스터 사용 및 세그먼트:오프셋 형식으로 지정한다.

  논리 주소        CPU의 주소 변환 방법        물리 주소

0x3030:4040    -> 0x30300 + 0x4040    ->     0x34340

0x????:4040    -> 0x30305 + 0x4040    ->     0x34345    -> 표현 불가, CPU가 0x10을 곱하기 때문 세

먼트 지정은 항상 16단위로만 이루어짐

0x3031:4040    -> 0x30310 + 0x4040   ->      0x34350

2. A20 Gate를 켜지 않은 상태에서 주소 지정이 가능하다고 해도 RAM 자체 물리주소 0x100000(1MB)로 사용 제한

3. A20 게이트를 켜면 0x100000 이상의 주소 접근 가능

2Protected Mode로 전환 전에 GDT(Global Descriptor Table)가 필요함

4. GDT는 어떤 형식이 있는 데이터의 나열이며, RAM 영역 중 어디든지 기입 가능(전원 OFF까지 그자리에 존재하여야 함)

5. GDT는 각 세그먼트 영역에 대해 어떻게 사용할지 설명해 놓음, 디스크립터는 아래와 같다.

Base Address : 세그먼트의 시작 주소, 물리주소로 하위 16비트와 상위 16비트로 두군데로 나누어 저장

Limit : 세그먼트의 한계점(크기)을 나타내며 오프셋은 이 숫자를 넘어갈 수 없다. 넘어간다면 GP fault(Protected Mode 규약 위반)가 발생한다. Limit는 총 20비트로 구성되며 2군데로 나누어 기재

  Limit는 또한 디스크립터 내 G 비트와 관련, G비트가 0이면 이 세그먼트 크기를 바이트 단위로 하며, 1이면       4KB 단위로 한다. 따라서 G 비트가 0일때, Limit 값이 그대로 한계점(크기)을 나타내고, 1일 때 Limit 값에     0xFFF 의 곱하여 그 수를 한계점으로 나타낸다. Limit 값이 0이면 크기가 0인 세그먼트라는 이야기이며 이렇     게 되어선 안됨)

Limit 값이 1일 때

G 비트가 0이면 세그먼트의 크기 : 1바이트

G 비트가 1이면 세그먼트의 크기 : 1 * 0xFFF = 0xFFF바이트가 된다.

Limit 값이 1234 일때

G 비트가 0이면 세그먼트의 크기 : 1234바이트

G 비트가 1이면 세그먼트의 크기 : 1234 * 0xFFF = 0x1234FFF바이트가 된다.

Limit 값이 20 비트를 모두 채워 0xFFFFF를 기재해 두었을 때

G 비트가 0이면 세그먼트의 크기 : 1메가바이트

G 비트가 1이면 세그먼트의 크기 : 0xFFFFF * 0xFFF = 0xFFFFFFFF바이트(4GB)가 된다.

P 비트 : 세그먼트가 메모리 상에 존재하는지를 나타내는 값, 커널 프로그램의 메모리 관리 루틴이 사용, 페이징 기능
          과 관련 있음

DPL(2Bits) : 이 세그먼트가 커널 레벨인지, 유저레벨인지 나타낸다. 인텔 x86 계열 CPU에서는 0~3의 값으로 4가                  지 레벨이 있으나, 보통의 경우 커널 제작 시 0과 3레벨만 사용. DPL 값이 0이면 이 세그먼트가 커널                  레벨이라는 것을 나타내고, 3(2진수 11)이면 이 세그먼트가 유저 레벨이라는 뜻

S : 이 세그먼트가 시스템 세그먼트인지(0일 때), 코드 혹은 데이터 세그먼트인지(1일 때)지정, 항상 1로 해둠 

Type(4Bits) : 디스크립터의 Type 적용시킬 4개 비트 값은 아래와 같다.

Type의 최상위 비트는 이 세그먼트가 코드 세그먼트인지 데이터 세그먼트인지 구분하여 지정

마지막 비트는 Access 비트이며 어떤 프로그램이 이 세그먼트에 접근했을 때 CPU가 이곳을 찾아 A비트를 1로 바꿔준다. 그러나 CPU는 이 비트를 0으로 클리어 해주지는 않음

커널은 메모리 관리를 할 때 이 비트가 1이 되었는지를 조사하거나, 액세스 된 세그먼트의 디스크립터를 찾아 이 A비트를 어느시간이 지나면 0으로 바꿔주는 일을 함

이 비트는 커널의 메모리 관리를 도와주는 의미, CPU의 동작에는 아무런 영향을 주지 않음, GDT 초기화 시 0 기재

코드 세그먼트일때와 데이터 세그먼트일 때의 두번째 비트와 세번째 비트의 역할이 달라짐

첫번째 비트가 0 일때

이 세그먼트는 데이터 세그먼트로 사용

두번째 비트가 1이면 EXPAND DOWN 형태

두번째 비트가 0이면 EXPAND UP 형태

이 세그먼트를 스택으로 사용할 경우 세그먼트의 크기가 동적으로 변하도록 만들기 위한 장치

비트를 1로 세트하고, 프로그램에서 도중에 이 세그먼트의 Limit 값을 바꾸면 스택의 제일 밑에 스택 공간이 추가된다.

스택을 위한 세그먼트는 항상 데이터 세그먼트여야 하며, 읽기 쓰기 모두 가능해야 한다. 만약 쓰기가 불가능한 세그먼트를 스택으로 사용 시, 다시말해 SS 세그먼트 레지스터에 쓰기가 불가능한 데이터 세그먼트를 로드하면 GP 폴트 발생

세번째 비트를 0으로 하면 이 세그먼트 영역은 읽기만 가능하고, 1로 세트 시 읽기 쓰기 모두 가능


첫번째 비트가 1 일때

이 세그먼트는 코드 세그먼트로 사용

두번째 비트가 1이면 Conforming을 지원(CPU의 Protected Mode의 보호 정책과 관련있음), 소스에선 0으로 해줌

세번째 비트가 1이면 읽기 가능, 0이면 실행만 가능하고 읽기 불가

코드 세그먼트의 경우 쓰기는 지원 안됨, 읽기가 되느냐 안되느냐 사항만 세팅 가능


디스크립터 내 D 비트는 이 세그먼트가 16비트인지 32비트인지 나타냄, 0이면 16비트, 1이면 32비트(커널 프로그램은 32비트이므로 1로 설정)


6. 위에서 만든 GDT는 어디에 만들어졌는지, 몇개나 있는지 확인하기 위해 CPU가 알도록 등록을 하여야함.

  -> CPU에는 GDTR이라는 레지스터가 존재, 48비트의 크기를 가지고 있음

 처음 16비트에는 이 프로그램에서 사용할 GDT의 크기가 들어감

 하위 32비트에는 GDT의 시작 주소가 물리주소로 들어감

 이렇게 2가지 값을 통해 CPU는 GDT가 어디에 있는지 알 수 있음

7. Real Mode 에는 CS, DS등 세그먼트 레지스터가 16비트의 한 워드로 구성되어 있음.

8. Protected Mode에서는 각 세그먼트 레지스터가 16비트의 셀렉터 레지스터64비트의 디스크립터 레지스터로 다시 나뉘어짐

9. 세그먼트 셀렉터(16비트) 중 상위 13비트(3~15비트)에는 디스크립터를 찾기 위한 인덱스가 들어있음.

10. 2번째 비트에는 TI의 값이 들어가고, 0비트와 1비트 두비트에는 RPL의 값이 들어감

11. 디스크립터의 앞쪽에 SysCodeSelector equ 0x08, SysDataSelector equ 0x10, VideoSelector equ 0x18 존재

12. 위 값은 세그먼트 셀렉터에 넣을 인덱스 값, TI, RPL을 포함하고 있음

13. 소스 위쪽의 mov bx, SysDataSelector

   mov ds, bx

또는

   mov ax, VideoSelector

   mov es, ax

   는 세그먼트 셀렉터에 값을 넣는 일을 한다.

세그먼트 셀렉터에 값이 들어가는 순간 CPU는 GDTR 레지스터에 등록되어 있는 GDT의 제일 앞 번지를 가지고 GDT를 찾아냄
그리고 세그먼트 셀렉터의 인덱스 값에 8(GDT에서 몇 번째의 디스크립터를 요구한다는 순서 값, 실제로 찾는 데에 물리주소가 사용되었기 때문에 8을 곱하여 물리주소의 자리를 찾아간다. 하나의 디스크립터가 8 바이트이기 때문, 그래서 세그먼트 셀렉터 값에 넣을때 0x8(8), 0x10(16), 0x18(24) .. 식으로 8씩 건넌 수를 넣게 된다.) 을 곱하여 인덱스로 요구한 디스크립터를 찾아냄
8은 2진수로 1000이며 셀렉터에 8을 넣으면 인덱스가 1이 될것이다. ( 3,4번째 비트는 RPL, 2번쨰는 TI의 값이 들어감)
16은 2진수로 10000이며 셀렉터에 16을 넣으면 인덱스는 10, 즉 2가 됨
24는 2진수로 11000이며 셀렉터에 24를 넣으면 인덱스는 11, 즉 3이 됨
디스크립터를 찾을 때 셀렉터에서 상위 13비트만 사용하고 3비트는 정보 전달에 쓰임
인덱스가 13비트이므로 GDT에 최대로 들어갈 수 있는 갯수는 8192이다.

14. DS 세그먼트 셀렉터 동작 원리

(1) 세그먼트 셀렉터에 10 입력 시 DS 세그먼트 레지스터에는 10진수 2가 들어가 2 * 8 = 16이 된다.

(2) GDTR에서 가져온 GDT의 제일 앞 번지에 이것을 더한다.

(3) 제일 앞 번지는 소스에서 lgdt로 GDT를 등록할 때 사용했던 변수를 보면 gdt+0x10000이라는 것을 알 수 있다.

(4) gdt + 0x10000 + 16 이라는 계산으로 DS가 가져야 할 디스크립터 값을 얻을 수 있으며 이 값은 DS 세그먼트 셀렉터로 요구한 GDT의 디스크립터의 번지가 된다.

(5) 이 디스크립터에서 DPL 값(커널 레벨, 유저 레벨 결정하는 값)을 세그먼트 셀렉터의 RPL 값과 대조한다.

(6) 위 값이 같다면 디스크립터의 내용을 DS 세그먼트 디스크립터 레지스터에 복사한다. 디스크립터와 디스크립터 레지스터의 생김새가 같으므로 그대로 들어가게 된다.

15. CPU의 동작 원리는 아래와 같다.

Protected Mode로 넘어간 후 CPU의 모드는 Protected Mode로 바뀌어 있지만 현재 해석 유닛읽기 유닛에는 Real Mode용 명령어들이 존재하고 있다.
Real Mode 명령어가 남아 있는 상태에서 진행된다면 다음 명령어부터는 Protected Mode에서 Real Mode 명령어를 실행하고 있는 꼴이 되어 프로그램 수행에 차질이 생길 가능성이 많다.
그래서 해석하는 유닛과 읽어 들이는 유닛에 존재하는 프로그램을 지워줄 필요가 있다. 이것을 하는 루틴이 소스 상 
jmp $+2
nop
nop
이다. 이 점프 명령어는 2개의 명령(2개의 NOP을 점프하여 그 다음 명령으로 가는데, JMP 명령은 CPU의 유닛에 남아 있는 명령들을 지워준다. 그 뒤 CPU는 Protected Mode로 되었지만 세그먼트 레지스터들이 16비트 값을 계속 포함하고 있다. 이것을 바꿔주어야 완전하게 Protected Mode가 된다.



부트스트랩을 이용하여 커널 로드 하기


1장에서 사용한 boot.txt를 이용하여 커널을 로드하여 본다.

여기서는 2개의 파일이 필요하다. 첫번째는 boot.txt파일, 두번째는 kernel.txt 파일이 필요하다.

첫번째 파일 boot.txt의 소스는 아래와 같다

boot.txt

[org 0]

[bits 16]

jmp 0x07C0:start

start:

mov ax, cs

mov ds, ax

mov ax, 0xB800

mov es, ax

mov di, 0

mov ax, word [msgBack]

mov cx, 0x7FF


paint:

mov word [es:di], ax

add di, 2

dec cx

jnz paint


read:

mov ax, 0x1000

mov es, ax

mov bx, 0

mov ah, 2    : 디스크에 있는 데이터를 es:bx의 주소로

mov al, 1    : 1섹터를 읽는다.

mov ch, 0    : 0번째 Cylinder

mov cl, 2    : 2번째 섹터부터 읽기 시작

mov dh, 0    : Head = 0

mov dl, 0    : Drive = 0, A: 드라이브

int 0x13    : Read 시작


jc read    : 에러가 날 경우 다시 함    : 이 루틴에서 에러 발생 시 CPU의 CF비트가 1로 세트됨,

1로 세트되어 있다면 erad: 번지로 점프하여 디스크 읽기를 다시 실행한다.

jmp 0x1000:0000    : kernel.bin이 위치한 곳으로 점프한다.



msgBack db '.', 0x67


times 510-($-$$) db 0

dw 0xAA55

------------------------------------------------------------------

kernel.txt

.[org 0]

[bits 16]


start:

mov ax, cs    : cs에는 0x1000이 들어있다. boot.txt 파일 마지막 jmp 0x1000:0000 에서 0x1000이 cs로 들어가기 때문

mov ds, ax

xor ax, ax

mov ss, ax


lea esi, [msgKernel]    : 문자열이 있는 곳의 주소를 구한다.

mov ax, 0xB800

mov es, ax    : es에 0xB800을 넣는다.

mov edi, 0    : 화면의 제일 처음 부분부터 시작한다.

call printf

jmp $


printf:

push eax    : 먼저 있던 eax 값을 스택에 보존해 놓는다.


printf_loop:

mov al, byte [esi]    : esi가 가리키는 주소에서 문자를 하나 가져온다.

mov byte [es:edi], al    : 문자를 화면에 나타낸다.

or al, al    : al이 0인지 알아본다.

jz printf_end    : 0이라면 print_end로 점프한다.

inc edi    : 0이 아니라면 edi를 1 증가시켜

mov byte [es:edi], 0x06    : 문자색과 배경색의 값을 넣는다.

inc esi    : 다음 문자를 꺼내기 위해 esi를 하나 증가

inc edi    : 화면에 다음 문자를 나타내기 위해 edi를 증가

jmp printf_loop    : 루프를 돈다.


printf_end:

pop eax    : 스택에 보존했던 eax를 다시 꺼낸다.

ret    : 호출한 부분으로 돌아간다.


msgKernel db "We are in kernel program", 0


아래와 같이 컴파일 및 copy 명령어를 통해 합친다.


floopy disk에 넣고 vm 실행 시 아래 그림과 같이 문자열이 출력된다.


커널 로드 성공!


+ Recent posts