인터럽트와 예외

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


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