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으로 나누었을 때 발생하는 예외를 구현한 것이다.

+ Recent posts