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

아래와 같이 소스를 짠다. (파일은 이전 장에서 사용하던 파일 중 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 명령이 내려지면 처음 인터럽트 걸렸던 명령문 다음 명령으로 돌아간다.


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




+ Recent posts