-
[OS] 18. InterruptsOS/OS from Scratch 2021. 7. 3. 16:43
서론
이번 강의에서는 Interrupt Service를 구현해보겠습니다. 해당 Github 강의는 다음과 같습니다.
https://github.com/cfenollosa/os-tutorial/tree/master/18-interrupts
cfenollosa/os-tutorial
How to create an OS from scratch. Contribute to cfenollosa/os-tutorial development by creating an account on GitHub.
github.com
이론
ISR (Interrupt Service Routine)
CPU가 A 작업을 하고 있다가 키보드 입력과 같은 Interrupt을 받으면 CPU는 현재 진행 중인 A 작업을 멈추고 Interrupt Sercie Routine으로 점프해 Interrupt에 관한 Routine을 처리합니다. Interrupt가 처리되면 원래 A 작업으로 돌아갑니다.
IDT (Interrupt Descriptor Table)
GDT와 매우 유사한 구조를 가지고 있는 IDT는 CPU에게 256개의 Interrupt에 대한 Handler을 찾게 하며, 중요한 첫 32개 Interrupt는 다음과 같습니다.
- 0 - Division by zero exception
- 1 - Debug exception
- 2 - Non maskable interrupt
- 3 - Breakpoint exception
- 4 - 'Into detected overflow'
- 5 - Out of bounds exception
- 6 - Invalid opcode exception
- 7 - No coprocessor exception
- 8 - Double fault (pushes an error code)
- 9 - Coprocessor segment overrun
- 10 - Bad TSS (pushes an error code)
- 11 - Segment not present (pushes an error code)
- 12 - Stack fault (pushes an error code)
- 13 - General protection fault (pushes an error code)
- 14 - Page fault (pushes an error code)
- 15 - Unknown interrupt exception
- 16 - Coprocessor fault
- 17 - Alignment check exception
- 18 - Machine check exception
- 19-31 - Reserved
#ifndef, #define
복잡한 프로그램의 경우 여러 헤더 파일이 꼬여, 헤더 파일의 중복 선언이 일어날 수 있습니다. 이를 막기 위해 해당 헤더 파일이 이미 선언된 경우, 중복된 선언을 방지하기 위해 #ifndef, #define을 사용합니다.
#ifndef TYPES_H #define TYPES_H ... 정의 #endif
위 코드(types.h)에서 만약 types.h가 이미 선언된 경우 재선언하지 않고, 선언되지 않는 경우 #define과 #endif 사이의 types.h의 내용을 선언합니다.
매크로 함수
매크로 함수는 #define 부분에 인자를 전달해 함수처럼 사용할 수 있는 매크로를 의미합니다.
#define SUM(X,Y) ((X)+(Y))
이때 매크로 함수의 인자는 어떤 타입이나 가능하며, 매크로 함수 내부에서 인자를 사용할 때는 괄호를 반드시 사용해야 합니다.
__attribute__((packed))
typedef struct { char c; int x; } myStruct;
상식적으로, myStruct의 크기는 char 1byte + int 4byte = 5byte이어야 합니다. 하지만 실제로 sizeof(myStruct)는 8입니다. 이는 CPU가 메모리에 4byte 단위로 접근할 시 가장 속도가 빠르기 때문에, 4byte 단위로 데이터를 저장하기 때문입니다. 이때 char는 4byte 공간을 차지하게 되고, 1byte를 제외한 나머지 3byte는 의미 없는 데이터로 채워지게 됩니다.
typedef struct { char c; int x; } __attribute__((packed)) myStruct;
myStruct가 메모리를 낭비하지 않고 5byte가 되도록 하기 위해서는 __attribute__((packed)) 특성을 설정해 컴파일러가 구조체의 실제 크기만큼 메모리를 할당하게 합니다.
__volatile__
__volatile__은 컴파일러가 코드를 최적화하지 않고, 있는 그대로 실행하게 합니다. 예를 들어, 컴파일러는 Inline Assembly에서 이후의 코드에서 사용하지 않는 변수를 변경하면, 해당 부분의 코드를 최적화해 수행하지 않습니다. __volatile__을 설정하면 이처럼 컴파일러에 의해 코드의 최적화가 발생하지 않습니다.
lidtl
lidtl Assembly 명령어는 IDT를 로딩하는 명령어입니다. lidtl 명령어는 IDT Description Structure의 주소를 인자로 받습니다.
Assembly에서 인자가 있는 함수 사용
Assembly에서 C 코드에 선언된 함수를 사용하기 위해서는 extern <함수 이름> 명령어로 해당 함수를 불러와 call <함수 이름> 명령어로 함수를 실행합니다. 이때 함수의 인자들은 Stack에 push 되어 전달됩니다. 즉, 다음과 같은 함수 func가 있을 때,
func(arg1, arg2, arg3)
func의 인자들은 오른쪽 인자들부터 Stack에 push 되어 사용합니다.
sti, cli
Assembly에서 sti는 Interrupt Flag (IF)를 IF=1인 상태로 만들고, cli는 IF=0인 상태를 만듭니다. IF=0이면 Interrupt가 발생하지 않게 억제되며, IF=1이면 Interrupt를 처리합니다.
코드
cpu/types.h
C 코드에서 char, int 데이터형을 통해 데이터의 크기를 지정하지 않고 직접 자료형을 만들어 사용하려 합니다. low_16은 메모리 주소에서 하위 16byte, high_16은 상위 주소를 계산하는 매크로 함수입니다.
#ifndef TYPES_H #define TYPES_H // 데이터 크기에 맞는 선언문 typedef unsigned int u32; typedef int s32; typedef unsigned short u16; typedef short s16; typedef unsigned char u8; typedef char s8; #define low_16(address) (u16)((address) & 0xffff) #define high_16(address) (u16)((address) >> 16 & 0xffff) #endif
cpu/idt.h
각 IDT는 크기가 256인 idt_gate_t 구조체 배열로 저장하고, idt_register_t는 GDT 레지스터와 비슷한 역할로 BIOS에 IDT를 로드합니다. 이때 모든 Interrupt의 구조를 나타낸 idt_gat_t 구조체는 handler의 주소, Kernel Segment Selector, Flags들로 이루어져 있습니다.
#ifndef IDT_H #define IDT_H #include "types.h" // Segment Selector #define KERNEL_CS 0x08 // 모든 Interrupt의 구조 typedef struct { // Handler 함수의 하위 16bit 주소 u16 low_offset; // Kernel Segment Selector u16 sel; // 무조건 0 u8 always0; // 7번 bit: Interrupt 존재 // 5-6번 bit: caller의 권한 (0=kernel, ..., 3=user) // 4번 bit: 0=Interrupt Gate // 3-0 bit: 1110 = 32bit Interrupt Gate u8 flags; // Handler 함수의 상위 16bit 주소 u16 high_offset; } __attribute__((packed)) idt_gate_t; // Interrupt Handler의 배열에 대한 포인터 // Assembly의 lidt으로 읽음 typedef struct { // 크기 u16 limit; // 주소 u32 base; } __attribute__((packed)) idt_register_t; // 256아니면 CPU 패닉 #define IDT_ENTRIES 256 idt_gate_t idt[IDT_ENTRIES]; idt_register_t idt_reg; void set_idt_gate(int n, u32 handler); void set_idt(); #endif
cpu/idt.c
idt.c에서 set_idt_gate 함수는 idt 배열의 원소를 채우는 역할을 하고, set_idt 함수는 idt_reg에 대해 lidtl Assembly 명령어를 불러 IDT를 탑재합니다.
#include "idt.h" #include "../kernel/util.h" void set_idt_gate(int n, u32 handler) { idt[n].low_offset = low_16(handler); idt[n].sel = KERNEL_CS; idt[n].always0 = 0; idt[n].flags = 0b10001110; idt[n].high_offset = high_16(handler); } void set_idt() { idt_reg.base = (u32) &idt; idt_reg.limit = IDT_ENTRIES*sizeof(idt_gate_t)-1; __asm__ __volatile__("lidtl (%0)" : : "r"(&idt_reg)); }
cpu/isr.h
isr.h와 isr.c에서는 32개의 가장 기본적인 Interrupt Service들을 정의하고 Handler들을 설정합니다. 이때 ISR들은 외부 Assembly 코드들에서 설정할 것이며, popa과정에서 나오는 레지스터들을 저장할 수 있도록 registers_t 구조체를 정의하겠습니다.
#ifndef ISR_H #define ISR_H #include "types.h" // CPU Exception들 extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31(); // 여러 레지스터 포함한 구조체 typedef struct { // Data Segment u32 ds; // pusha로 pushed되는 레지스터 u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; // Interrupt 번호와 에러 코드 u32 int_no, err_code; // CPU에 의해 자동적으로 pushed 됨 u32 eip, cs, eflags, useresp, ss; } registers_t; void isr_install(); void isr_handler(registers_t r); #endif
cpu/isr.c
isr.c에서는 32개의 Interrupt Service Routine을 설정하고 ISR이 발생했을 때 어떤 Interrupt가 발생했는지 출력하는 Handler를 구현합니다.
#include "isr.h" #include "idt.h" #include "../drivers/screen.h" #include "../kernel/util.h" void isr_install() { set_idt_gate(0, (u32)isr0); set_idt_gate(1, (u32)isr1); set_idt_gate(2, (u32)isr2); set_idt_gate(3, (u32)isr3); set_idt_gate(4, (u32)isr4); set_idt_gate(5, (u32)isr5); set_idt_gate(6, (u32)isr6); set_idt_gate(7, (u32)isr7); set_idt_gate(8, (u32)isr8); set_idt_gate(9, (u32)isr9); set_idt_gate(10, (u32)isr10); set_idt_gate(11, (u32)isr11); set_idt_gate(12, (u32)isr12); set_idt_gate(13, (u32)isr13); set_idt_gate(14, (u32)isr14); set_idt_gate(15, (u32)isr15); set_idt_gate(16, (u32)isr16); set_idt_gate(17, (u32)isr17); set_idt_gate(18, (u32)isr18); set_idt_gate(19, (u32)isr19); set_idt_gate(20, (u32)isr20); set_idt_gate(21, (u32)isr21); set_idt_gate(22, (u32)isr22); set_idt_gate(23, (u32)isr23); set_idt_gate(24, (u32)isr24); set_idt_gate(25, (u32)isr25); set_idt_gate(26, (u32)isr26); set_idt_gate(27, (u32)isr27); set_idt_gate(28, (u32)isr28); set_idt_gate(29, (u32)isr29); set_idt_gate(30, (u32)isr30); set_idt_gate(31, (u32)isr31); set_idt(); } char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor", "Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt", "Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" }; void isr_handler(registers_t r) { kprint("Received Interrupt: "); char s[3]; int_to_ascii(r.int_no, s); kprint(s); kprint("\n"); kprint(exception_messages[r.int_no]); kprint("\n"); }
cpu/interrupt.asm
interrupt.asm에서는 각 ISR들을 구현합니다. 먼저 isr<isr 번호>에서 각 ISR마다 에러코드와 Interrupt번호를 설정하고 isr_common_stub를 호출해 isr.c의 isr_handler함수에 인자를 전달해 호출합니다. isr_handler함수의 처리가 완료되면 원래 상태로 레지스터 값들을 되돌려 놓습니다.
; isr.c의 isr_handler() [extern isr_handler] ; Common ISR Code isr_common_stub: ; 1. CPU상태 저장 ; (인자) edi, esi, ebp, esp, ebx, edx, ecx, eax push pusha ; EAX의 하위 16bit = ds mov ax, ds ; (인자) Data Segment Descriptor 저장 push eax ; Kernel Data Segment Descriptor mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax ; 2. C handler 호출 call isr_handler ; 3. 원래 상태로 되돌려놓음 pop eax mov ds, ax mov es, ax mov fs, ax mov gs, ax popa ; push된 에러코드와 ISR 번호 삭제 add esp, 8 sti ; pop cs, eip, eflags, ss, esp iret ; 여러 handler에 대한 interrupt handler 정의 ; 일부 interrupt는 에러코드를 stack에 저장 global isr0 global isr1 global isr2 global isr3 global isr4 global isr5 global isr6 global isr7 global isr8 global isr9 global isr10 global isr11 global isr12 global isr13 global isr14 global isr15 global isr16 global isr17 global isr18 global isr19 global isr20 global isr21 global isr22 global isr23 global isr24 global isr25 global isr26 global isr27 global isr28 global isr29 global isr30 global isr31 ; 0: Divide by zero division isr0: cli ; (인자) 에러 코드 (더미) push byte 0 ; (인자) interrupt 번호 push byte 0 jmp isr_common_stub ; 1: Debug Exception isr1: cli push byte 0 push byte 1 jmp isr_common_stub ; 2: Non Maskable Interrupt Exception isr2: cli push byte 0 push byte 2 jmp isr_common_stub ; 3: Int 3 Exception isr3: cli push byte 0 push byte 3 jmp isr_common_stub ; 4: INTO Exception isr4: cli push byte 0 push byte 4 jmp isr_common_stub ; 5: Out of Bounds Exception isr5: cli push byte 0 push byte 5 jmp isr_common_stub ; 6: Invalid Opcode Exception isr6: cli push byte 0 push byte 6 jmp isr_common_stub ; 7: Coprocessor Not Available Exception isr7: cli push byte 0 push byte 7 jmp isr_common_stub ; 8: Double Fault Exception isr8: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 8 jmp isr_common_stub ; 9: Coprocessor Segment Overrun Exception isr9: cli push byte 0 push byte 9 jmp isr_common_stub ; 10: Bad TSS Exception isr10: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 10 jmp isr_common_stub ; 11: Segment Not Present Exception isr11: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 11 jmp isr_common_stub ; 12: Stack Fault Exception isr12: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 12 jmp isr_common_stub ; 13: General Protection Fault Exception isr13: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 13 jmp isr_common_stub ; 14: Page Fault Exception isr14: cli ; (인자) 에러 코드 Stack에 이미 push됨 push byte 14 jmp isr_common_stub ; 15: Reserved Exception isr15: cli push byte 0 push byte 15 jmp isr_common_stub ; 16: Floating Point Exception isr16: cli push byte 0 push byte 16 jmp isr_common_stub ; 17: Alignment Check Exception isr17: cli push byte 0 push byte 17 jmp isr_common_stub ; 18: Machine Check Exception isr18: cli push byte 0 push byte 18 jmp isr_common_stub ; 19: Reserved isr19: cli push byte 0 push byte 19 jmp isr_common_stub ; 20: Reserved isr20: cli push byte 0 push byte 20 jmp isr_common_stub ; 21: Reserved isr21: cli push byte 0 push byte 21 jmp isr_common_stub ; 22: Reserved isr22: cli push byte 0 push byte 22 jmp isr_common_stub ; 23: Reserved isr23: cli push byte 0 push byte 23 jmp isr_common_stub ; 24: Reserved isr24: cli push byte 0 push byte 24 jmp isr_common_stub ; 25: Reserved isr25: cli push byte 0 push byte 25 jmp isr_common_stub ; 26: Reserved isr26: cli push byte 0 push byte 26 jmp isr_common_stub ; 27: Reserved isr27: cli push byte 0 push byte 27 jmp isr_common_stub ; 28: Reserved isr28: cli push byte 0 push byte 28 jmp isr_common_stub ; 29: Reserved isr29: cli push byte 0 push byte 29 jmp isr_common_stub ; 30: Reserved isr30: cli push byte 0 push byte 30 jmp isr_common_stub ; 31: Reserved isr31: cli push byte 0 push byte 31 jmp isr_common_stub
kernel/kernel.c
kernel.c에서 Interrupt를 호출합니다.
#include "../drivers/screen.h" #include "util.h" #include "../cpu/isr.h" #include "../cpu/idt.h" void main() { isr_install(); __asm__ __volatile__("int $2"); __asm__ __volatile__("int $3"); }
boot/bootsect.asm
bootsect.asm에서 kernel이 차지하는 sector를 31로 늘려 공간을 확보합니다.
[bits 16] load_kernel: mov bx, MSG_LOAD_KERNEL call print call print_nl mov bx, KERNEL_OFFSET mov dh, 31 mov dl, [BOOT_DRIVE] call disk_load ret
Makefile
Makefile에 cpu path를 추가해줍니다.
C_SOURCES = $(wildcard kernel/*.c drivers/*.c cpu/*.c) HEADERS = $(wildcard kernel/*.h drivers/*.h cpu/*.h)
실행 결과
make로 실행하면, 2, 3 Interrupt가 발생한 것을 확인할 수 있습니다..
실행결과 'OS > OS from Scratch' 카테고리의 다른 글
[OS] 20. Interrupt-timer (0) 2021.07.18 [OS] 19. Interrupt-irqs (0) 2021.07.12 [OS] 17. Video Scroll (0) 2021.07.02 [OS] 16. Video Driver (0) 2021.07.02 [OS] 15. Video Ports (0) 2021.06.30