본문 바로가기
OS/만들면서 배우는 OS 커널의 구조와 원리

4. 인터럽트와 예외

by 내용증명장인 2026. 4. 10.

인터럽트가 어떤 과정을 통해 발생하고 처리하는지 알아볼 수 있다.

 IDT(Interrupt Descriptor Table)

인터럽트란?

CPU가 작업을 처리하는 도중에 처리해야할 다른 일이 발생할 수 있다.
이때 인터럽트를 통해 해당 작업의 흐름을 끊어주는 역할을 한다.
하드웨어 인터럽트와 소프트웨어 인터럽트로 나뉘며 해당 장에서는 인터럽트 발생 시 커널에서 발생하는 과정을 알아볼 것이다.


IDT는 GDT랑 개념이 비슷하고 연관도 있다.

인터럽트가 발생하면 해당 인터럽트를 처리하기 위한 서브루틴이 필요하다. 이 서브루틴을 인터럽트 핸들러라고 한다.
IDT는 256개의 디스크립터로 구성되어 있으며, 인터럽트 발생 시 어느 루틴(인터럽트 핸들러)을 실행시켜야 하는지에 대한 정보를 포함하고 있다.

인터럽트 핸들러는 GDT에 존재하며, GDT 디스크립터 형태로 존재하고 있다.

위 그림은 IDT 디스크립터의 구조이다.
GDT에서 사용했던 비트와 의미가 같다.
P는 메모리 상에 존재하는지, DPL은 권한, D비트는 16비트인지 32비트인지를 의미한다.

참고로 IDT는 GDT처럼 NULL 디스크립터를 가지고 있지 않다.

IDTR

IDT도 GDTR처럼 IDTR이 존재한다.

lidt 명령어를 통해 메모리 상의 IDT 시작 주소를 찾는다.

인터럽트가 발생되면 다음과 같은 과정을 거친다.

1. 0x20 인터럽트 발생
2. CPU는 IDTR을 참조하여 IDT 시작 주소 파악 후 인터럽트 번호(0x20, 32)를 통해 해당 IDT 디스크립터를 찾는다.
3. 코드 세그먼트 셀렉터(GDT에서 세그먼트 셀렉터)를 통해 GDT 디스크립터를 찾는다. (당연히 gdtr을 통해 base address를 구한 후 찾음)
4. 이제 인터럽트 핸들러의 세그먼트 위치와 오프셋을 알고 있으니, 세그먼트 내 실제 핸들러 위치를 찾는다.
5. 핸들러가 실행되어 iret 명령어가 실행되면 인터럽트가 걸린 코드의 다음 코드로 돌아간다.


PIC(Programmable Interrupt Controller)

PIC는 하드웨어 인터럽트와 관련이 있다.
우선 소스를 작성하였다.

init.inc

SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector   equ 0x18

boot2.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
    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
    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

    cli

    mov al, 0x11
    out 0x20, al
    dw 0x00eb, 0x00eb
    out 0xA0, al
    dw 0x00eb, 0x00eb

    mov al, 0x20
    out 0x21, al
    dw 0x00eb, 0x00eb
    mov al, 0x28
    out 0xA1, al
    dw 0x00eb, 0x00eb

    mov al, 0x04
    out 0x21, al
    dw 0x00eb, 0x00eb
    mov al, 0x02
    out 0xA1, al
    dw 0x00eb, 0x00eb

    mov al, 0x01
    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

    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 0x8000FFFF, 0x0040920B
gdt_end:

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

 

kernel2.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
    mov edi, 0

loop_idt:
    lea esi, [idt_ignore]
    mov cx, 8
    rep movsb
    dec ax
    jnz loop_idt

    mov edi, 8*0x20
    lea esi, [idt_timer]
    mov cx, 8
    rep movsb

    lidt [idtr]
    mov al, 0xFE
    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 ignoreable interrupt", 0
msg_isr_32_timer db ".This is the timer interrupt", 0

idtr:
    dw 256*8-1
    dd 0

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
    dw 0x08
    db 0
    db 0x8E
    dw 0x0001

times 512-($-$$) db 0

 

이들을 컴파일 후 부팅하면 다음과 같은 화면이 출력된다.

This is the timer interrupt 앞의 문자가 지속적으로 변하는 것을 확인 가능하다.


 

PIC 구성

PIC는 위와 같이 구성되어 있다.
Master는 IRQ 번호가 0~7까지, Slave는 8~15번까지 있다.

마스터 PIC에 연결된 장치에서 인터럽트 발생 시

1. 마스터 PIC는 CPU에게 INT 핀을 통해 신호 전송
2. CPU는 EFLAG 레지스터의 IE 비트 1로 설정 후 마스터 PIC에게 /INTA를 통해 수신 완료 신호 전송
3. 마스터 PIC는 신호 수신 후 인터럽트가 발생한 IRQ 번호를 데이터 버스를 통해 CPU로 전달
4. CPU는 해당 데이터를 통해 IDT 디스크립터를 찾아 인터럽트 핸들러 실행

슬레이브 PIC에 연결된 장치에서 인터럽트 발생 시

1. 슬레이브 PIC는 마스터 PIC에게 INT 핀을 통해 신호 전송
2. 마스터 PIC는 INT 핀을 통해 CPU에게 신호 전송
3. CPU가 /INTA 신호를 마스터 PIC에게 보내면 마스터 PIC는 슬레이브 PIC에게 전달, 슬레이브 PIC는 신호를 전달받고 데이터 버스를 통해 IRQ 번호(8~15)를 전달한다


ICW

마스터 PIC, 슬레이브 PIC 설정을 위해 초기화를 통해 프로그래밍 해주어야 한다.
ICW가 그러한 역할을 하며 4가지로 구성되어 있으며, ICW는 8비트로 이루어져 있다.

ICW1

PIC를 초기화 하는 명령어이다.
슬레이브를 사용할 것 인지, ICW4를 사용할 것인지 결정한다.

ICW1 구조

LTIM
0에서 1로 변환 시 인터럽트로 간주
1 지속 시 계속 인터럽트

SNGL
0 : 마스터 / 슬레이브 형식 사용
1 : 마스터만 사용

IC4
ICW4가 추가적으로 필요한지 나타냄

ICW2

인터럽트 발생 시 기준 값 + IRQ 번호를 CPU에게 전송하는데, 그 기준 값을 지정한다.

ICW2 구조

해당 구조에 기준 값이 들어가게 된다.
10진수로 8단위 씩만 지정 가능하다.
기준 값 + IRQ 번호를 통해 IDT 디스크립터 인덱스를 찾는다.
일반적으로 0x20을 사용한다.

ICW3

마스터 / 슬레이브 PIC가 어디에 연결되었는지 결정한다.

마스터 PIC에서의 ICW3 구조

0 : 해당 IRQ는 하드웨어 장치에 연결됨
1 : 해당 IRQ는 슬레이브 PIC에 연결됨

슬레이브 PIC에서의 ICW3 구조

슬레이브 PIC에서 마스터 PIC로 연결된 IRQ가 몇인지 숫자로 나타낸다.

ICW4

추가 명령어이며 ICW1에서 IC4 옵션을 활성화 해야한다.
수동 / 자동 리셋 설정, 8086 모드 설정을 한다.

ICW4

SFNM
대규모 시스템에서 사용

BUF
외부 버퍼 사용 여부

M/S
BUF가 1일 때 유효한 옵션, 칩이 마스터인지 슬레이브인지 결정

AEOI
인터럽트가 끝났을 때 자동으로 알릴지 결정
1: 리셋 자동 수행, CPU에게 IRQ 번호를 알린 후 바로 PIC 리셋
0: 리셋 수동 수행, CPU에서 IRQ 번호를 받은 뒤 인터럽트 핸들러에서 인터럽트 처리 후 코드 마지막에 EOI (0x20)을 통해 PIC에게 명령하여 리셋
- 여기서 리셋은 초기화가 아닌, 다음 인터럽트를 받을 준비를 뜻한다.

UPM
80/85모드를 사용할 것인지, 8086 모드를 사용할 것인지 결정


소스코드 분석

mov al, 0x11
out 0x20, al
dw 0x00eb, 0x00eb
out 0xA0, al
dw 0x00eb, 0x00eb

boot2.asm의 일부이다.
ICW1 관련 코드이며 값은 0x11(0001 0001)이다.
즉, IC4가 활성화 되어있어 ICW4를 사용하고 마스터 / 슬레이브 모두 사용한다.

0x00eb는 jmp $+2를 기계어로 바꾼 것이며 시간 지연를 위해 사용한 것이다.
이후 out 0xA0, al 이 실행되는데, out은 CPU 레지스터에 있는 데이터를 I/O포트로 밀어넣는 명령어이다.
즉, out 0x20, al은 마스터 PIC I/O포트 0x20과 0x21에 프로그래밍,
out 0xA0, al은 슬레이브 PIC I/O포트 0xA0과 0xA1에 프로그래밍한다.

mov al, 0x20
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x28
out 0xA1, al
dw 0x00eb, 0x00eb

해당 코드는 ICW2 명령코드이다.
마스터 PIC의 기준 값은 0x20, 슬레이브 PIC는 0x28이다.
마스터 PIC의 IRQ 1번에서 인터럽트 발생 시 기준 값인 0x20 + IRQ 1을 더해서 0x21 이 된다.
0x21은 10진수로 33인데, IDT 디스크립터의 33번째 인덱스를 찾아가게 된다.

mov al, 0x04
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x02
out 0xA1, al
dw 0x00eb, 0x00eb

해당 코드는 ICW3 명령 코드이다.

마스터 PIC ICW3의 2번 비트에 1을 설정(0x04)하여 2번 IRQ에 슬레이브 PIC가 연결되어있음을 알린다.
그리고 슬레이브 PIC ICW3은 마스터 PIC와 몇 번 IRQ로 연결되었는지 숫자로 알린다.
마스터 PIC가 0x04(0000 0100)이므로 IRQ 2번, 슬레이브는 0x02 이므로 마스터 PIC의 2번 IRQ에 연결되어있다는 것을 슬레이브 PIC에게 알려준다.

mov al, 0x01
out 0x21, al
dw 0x00eb, 0x00eb
out 0xA1, al
dw 0x00eb, 0x00eb

해당 코드는 ICW4 명령 코드이다.
0x01 이므로 8086 모드를 사용한다는 의미가 된다.

mov al, 0xFF
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0xFB
out 0x21, al

해당 코드는 모든 인터럽트를 막아놓는 부분이다.


인터럽트 핸들러

타이머 인터럽트 핸들러 구현

mov edi, 8*0x20
lea esi, [idt_timer]
mov cx, 8
rep movsb

해당 코드는 PIC의 IRQ 0번인 타이머이다.
0x20을 통해 인터럽트 하는 것을 확인할 수 있다.
IDT 디스크럽터의 0x20번째 위치의 목적지를 설정하기 위해 8을 곱하였다.

lidt [idtr]
mov al, 0xFE
out 0x21, al
sti

jmp $

idt_timer 디스크립터이며 하드웨어 인터럽트 중 타이머에 관한 것만 열어주는 코드이다.
IRQ 0번인 0번 비트를 0으로 설정 후 0x21번 포트에 out 명령을 하는 코드이다.
sti를 통해 CPU 측에서 /INTA 신호를 되돌려준다.
그리고 jmp 명령어를 통해 무한루프를 돌게 된다.

idt_timer:
    dw isr_32_timer
    dw 0x08
    db 0
    db 0x8E
    dw 0x0001

인터럽트 핸들러의 물리주소 설정 코드이다.

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

인터럽트 핸들러이다.


키보드 인터럽트 핸들러 구현

init.inc

SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector   equ 0x18

boot2.asm은 그대로 사용하였다.

kernel3.asm

%include "init.inc"

[org 0x010000]
[bits 32]

PM_Start:
    mov bx, SysDataSelector
    mov ds, 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
    mov edi, 0

loop_idt:
    lea esi, [idt_ignore]
    mov cx, 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
    lea esi, [idt_keyboard]
    mov cx, 8
    rep movsb

    lidt [idtr]

    mov al, 0xFC
    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 ignoreable 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
    dd 0

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

    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_timer:
    dw isr_32_timer
    dw 0x08
    db 0
    db 0x8E
    dw 0x0001

idt_keyboard:
    dw isr_33_keyboard
    dw 0x08
    db 0
    db 0x8E
    dw 0x0001

times 512-($-$$) db 0

이를 컴파일 하여 실행하면 다음과 같은 화면이 출력된다.

키보드를 누르면 keyboard interrupt 문장이 출력되고 문장 앞 문자가 바뀌는 것을 확인 가능하다.


소스코드 해석

mov edi, 8*0x21 
lea esi, [idt_keyboard] 
mov ex. 8 
rep movsb

위 코드는 IDT에 디스크립터를 복사하는 코드이다.

dt_key board: 
	dw isr_33_keyboard 
	dw Ox08 
	db 0 
	db Ox8E 
	dw Ox0001

 

IDT 주소와 구조 설정 코드이다.

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

위 코드는 인터럽트 핸들러이며, in al, 0x60 명령을 통해 키보드 버퍼에 있는 문자 스캔코드를 가져온다.
무슨 코드인지는 구별하지 않고 가져오기만 하는 코드이다.


예외(Exception)

ICW2의 마스터 PIC의 기준 값을 주로 0x20 으로 잡는다 했는데, 그 이유가 앞 번호에 이미 할당된 예외들이 있기 때문이다.
31번까지 예약된 인터럽트들이 존재하며 32번부터 유저 정의 인터럽트이다.

이때문에 PIC는 초기화를 진행하여 인터럽트가 충돌하지 않도록 한다.

'OS > 만들면서 배우는 OS 커널의 구조와 원리' 카테고리의 다른 글

6. 보호  (0) 2026.04.13
5. 태스크 스위칭, 문맥 교환  (0) 2026.04.13
3. Protected Mode로 변환  (1) 2026.04.08
2. 커널 로드  (0) 2026.04.06
1. 부트스트랩  (0) 2026.04.03