How to write your own Operating System (x86_64): Interrupts
Πως να γράψετε το δικό σας Λειτουργικό Σύστημα (x86_64): Διακοπές
IntroductionΕισαγωγή
In our previous articles, we showed how to implement a bootloader and how to enter the long mode. In this part, we will discuss the Interrupts. An Interrupt is a signal emitted by hardware or software when a process or an event needs attention. Usually, an Interrupt Vector Table (IVT) is used to determine the proper response to three types of events: Hardware Interrupts, Software Interrupts, and Processor Exceptions. In the x86_64 architecture, this data structure is named Interrupt Descriptor Table (IDT) and consists of 256 interrupt vectors, the first 32 of which (0–31 or 0x00–0x1F) are reserved for the processor exceptions.
Σε προηγούμενα άρθρα μας, είδαμε πώς να υλοποιήσουμε έναν εκκινητή και πώς να μπούμε στη λειτουργία μακράς κατάστασης. Σε αυτό το μέρος, θα συζητήσουμες τις Διακοπές. Μια Διακοπή είναι ένα σήμα που παράγεται από το υλικό ή το λογισμικό όταν μια διεργασία ή ένα συμβάν χρειάζεται προσοχή. Σύνηθως, ένας Διανυσματικός Πίνακας Διακοπών (Interrupt Vector Table, IVT) χρησιμοποιείται για να καθοριστεί η κατάλληλη απάντηση σε τρια ήδη συμβάντων: Διακοπές Υλικού (Hardware Interrupts), Διακοπές Λογισμικού (Software Interrupts) και Εξαιρέσεις Επεξεργαστή (Processor Exceptions). Στην αρχιτεκτονική x86_64, αυτή η δομή δεδομένων ονομάζεται Πίνακας Περιγραφής Διακοπών (Interrupt Descriptor Table, IDT) και αποτελείται από 256 διανύσματα Διακοπών, από τα οποία τα 32 πρώτα (0–31 ή 0x00–0x1F) είναι δεσμευμένα για τις Εξαιρέσεις του Επεξεργαστή.
Kernel Code: InterruptsΚώδικας του Πυρήνα: Διακοπές
kernel/idt.asm
In this file, we define our Interrupt Descriptor Table (IDT). For the time being, we have set up Interrupt Services Routines (ISR) for some CPU Exceptions ("Division by zero", "General Protection Fault" and "Page Fault") and for two IRQs, IRQ0 (Programmable Interval Timer) and IRQ1 (Keyboard). The value 0x8F means a "Trap Gate", while the value 0x8E means an "Interrupt gate". The important difference between trap and interrupt gates is that interrupt gates will disable further processor handling of maskable interrupts.
Σε αυτό το αρχείο, ορίζουμε τον Πίνακα Περιγραφής Διακοπών (Interrupt Descriptor Table, IDT). Προς το παρόν, έχουμε ορίσει Ρουτίνες Εξυπηρέτησης Διακοπών (Interrupt Services Routines, ISR) για μερικες Εξαιρέσεις του Επεξεργαστή ("Διαίρεση με το μηδέν", "Γενικό Σφάλμα Προστασίας" και "Σφάλμα Σελίδας") και για δύο IRQs, τα IRQ0 (Χρονιστής Προγραμματιζόμενης Περιόδου) και IRQ1 (Πληκτρολόγιο). Η τιμή 0x8F σημαίνει μια "Πύλη Παγίδευσης (Trap Gate)", ενώ η τιμή 0x8E σημαίνει μια "Πύλη Διακοπής (Interrupt Gate)". Η σημαντική διαφορά ανάμεσα στις πύλες Παγίδευσης και Διακοπής είναι ότι οι Πύλες Διακοπών απενεργοποιούν τον χειρισμό επιπλέον παρεμποδιζόμενων/αποκρύψιμων (maskable) Διακοπών.
;**************************************************************************************;
; Interrupt Descriptor Table (IDT) ;
;**************************************************************************************;
; The Interrupt Descriptor Table (IDT) is a data structure used to implement an ;
; Interrupt Vector Table (IVT), i.e. to determine the proper response to three types ;
; of events: hardware interrupts, software interrupts, and processor exceptions. ;
; The IDT consists of 256 interrupt vectors, the first 32 (0–31 or 0x00–0x1F) of which ;
; are used for processor exceptions. ;
;**************************************************************************************;
BASE_OF_SECTION equ 0x8000
; We use a macro to simplify a little each IDT entry
%macro .idtentry 3
dw ((BASE_OF_SECTION + %1 - $$) & 0xFFFF) - 1024 ; Low word bits (0-15) of offset
dw %2 ; Code-Segment-Selector
db 0 ; Always zero
db %3 ; Type and Attributes
dw ((BASE_OF_SECTION + %1 - $$) >> 16) & 0xFFFF ; Middle bits (16-31) of offset
dd ((BASE_OF_SECTION + %1 - $$) >> 32) & 0xFFFFFFFF ; High bits (32-64) of offset
dd 0 ; Reserved
%endmacro
IDT_START:
;*************************************************************************************
; IDT Entry: Address of Interrupt Service Routine, Code Segment Selector, Attributes ;
;*************************************************************************************
.idtentry ISR_Division_by_Zero, CODE_SEG, 0x8F ;0 (Division by zero)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;1 (Debug Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;2 (NMI, Non-Maskable Interrupt)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;3 (Breakpoint Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;4 (INTO Overflow)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;5 (Out of Bounds)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;6 (Invalid Opcode)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;7 (Device Not Available)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;8 (Double Fault)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;9 (Deprecated)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;10 (Invalid TSS)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;11 (Segment Not Present)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;12 (Stack-Segment Fault)
.idtentry ISR_GPF , CODE_SEG, 0x8F ;13 (General Protection Fault)
.idtentry ISR_Page_Fault , CODE_SEG, 0x8F ;14 (Page Fault)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;15 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;16 (x87 Floating-Point Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;17 (Alignment Check Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;18 (Machine Check Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;19 (SIMD Floating-Point Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;20 (Virtualization Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;21 (Control Protection Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;22 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;23 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;24 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;25 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;26 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;27 (Reserved)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;28 (Hypervisor Injection Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;29 (VMM Communication Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;30 (Security Exception)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;31 (Reserved)
.idtentry ISR_systimer , CODE_SEG, 0x8F ;32 (IRQ0: Programmable Interval Timer)
.idtentry ISR_keyboard , CODE_SEG, 0x8E ;33 (IRQ1: Keyboard)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;34 (IRQ2: PIC Cascade, used internally)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;35 (IRQ3: COM2, if enabled)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;36 (IRQ4: COM1, if enabled)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;37 (IRQ5: LPT2, if enabled)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;38 (IRQ6: Floppy Disk)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;39 (IRQ7: LPT1)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;40 (IRQ8: CMOS real-time clock)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;41 (IRQ9: Free)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;42 (IRQ10: Free)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;43 (IRQ11: Free)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;44 (IRQ12: PS2 Mouse)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;45 (IRQ13: Coprocessor)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;46 (IRQ14: Primary ATA Hard Disk)
.idtentry ISR_dummy , CODE_SEG, 0x8F ;47 (IRQ15: Secondary ATA Hard Disk)
; ...
; Although the IDT can contain less than 256 entries, any entries that are not present
; will generate a General Protection Fault when an attempt to access them is made.
IDT_END:
; The IDTR is the argument for the LIDT assembly instruction
; which loads the location of the IDT to the IDT Register.
ALIGN 4
IDTR:
.Length dw IDT_END-IDT_START-1 ; One less than the size of the IDT in bytes.
.Base dd IDT_START ; The linear address of the Interrupt Descriptor Table
; (not the physical address, paging applies).
kernel/video.asm
Before we go any further, in order to get some feedback, we have to write some code that print messages on the screen. We cannot depend on the BIOS services any more, because the BIOS is designed to run in real mode (and we are now in long mode). Thus, we have to write our own graphics driver that access the video memory directly:
Πριν προχωρήσουμε παρακάτω, για να μπορούμε να έχουμε κάποια ανάδραση για το τι συμβαίνει, πρέπει να γράψουμε κάποιον κώδικα που θα εκτυπώνει μηνύματα στην οθόνη μας. Δεν μπορούμε να βασιζόμαστε πλέον στις υπηρεσίες του BIOS, μιας και το BIOS έχει σχεδιαστεί για να τρέχει σε λειτουργία πραγματικής κατάστασης (real mode), ενώ εμείς βρισκόμαστε τώρα σε λειτουργία μακράς κατάστασης (long mode). Οπότε, πρέπει να γράψουμε τον δικό μας οδηγό γραφικών (graphics driver) ο οποίος θα επεξεργάζεται απευθείας τη μνήμη των γραφικών:
BITS 64
;---Initialized data----------------------------------------------------------------
VRAM dq 0xB8000
;---Constants-----------------------------------------------------------------------
VGA_WIDTH equ 80
VGA_HEIGHT equ 25
; Colors
VGA_COLOR_BLACK equ 0
VGA_COLOR_BLUE equ 1
VGA_COLOR_GREEN equ 2
VGA_COLOR_CYAN equ 3
VGA_COLOR_RED equ 4
VGA_COLOR_MAGENTA equ 5
VGA_COLOR_BROWN equ 6
VGA_COLOR_LIGHT_GREY equ 7
VGA_COLOR_DARK_GREY equ 8
VGA_COLOR_LIGHT_BLUE equ 9
VGA_COLOR_LIGHT_GREEN equ 10
VGA_COLOR_LIGHT_CYAN equ 11
VGA_COLOR_LIGHT_RED equ 12
VGA_COLOR_LIGHT_MAGENTA equ 13
VGA_COLOR_LIGHT_BROWN equ 14
VGA_COLOR_WHITE equ 15
;---Code---------------------------------------------------------------------------
Fill_screen:
;*********************************************************************************;
; Fill screen ;
;---------------------------------------------------------------------------------;
; rax (XY__XY__XY__XY__): X -> Background color, Y -> Character color ;
; rax (__ZZ__ZZ__ZZ__ZZ): ASCII code(s) of character(s) to use to fill the screen ;
;*********************************************************************************;
mov rdi, [VRAM]
mov rcx, 500 ; 80*25 / 4 = 500 (we set 4 characters each time).
rep stosq ; Clear the entire screen.
ret
Print:
;**********************************************************************************;
; Prints a string ;
;----------------------------------------------------------------------------------;
; rsi: pointer to string (first 16 bits = the number of characters in the string.) ;
; ah: Color attributes ;
; r8: x ;
; r9: y ;
;**********************************************************************************;
push rsi
push rax
push r8
push r9
dec r8
add r8, r8
dec r9
mov rdi, [VRAM]
push rax
mov rax, VGA_WIDTH*2
mul r9
add r8, rax
pop rax
mov cx, word [rsi] ; first 16 bits = the number of characters in the string
inc rsi
.string_loop: ; print all the characters in the string
lodsb
mov al, byte [rsi]
mov [rdi+r8], ax
add rdi, 2
loop .string_loop
pop r9
pop r8
pop rax
pop rsi
ret
Print_hex:
;**********************************************************************************;
; Prints a 16-digit hexadecimal value ;
;----------------------------------------------------------------------------------;
; r10: value to be printed ;
; ah: Color attributes ;
;**********************************************************************************;
push r10
sub rsp, 20 ; make space for the string length (2 bytes) and 18 characters
mov rsi, rsp
push rsi ; store rsi (string address)
mov [rsi], word 18 ; string length = 17
mov [rsi+2], word "0x"
add rsi, 19 ; point rsi to the end of the string
mov ecx, 16 ; loop 16 times (one for each digit)
.digit:
push r10 ; store rax
and r10, 0Fh ; isolate digit
add r10b,'0' ; convert to ascii
cmp r10b,'9' ; is hex?
jbe .nohex
add r10b, 7 ; hex
.nohex:
mov [rsi], byte r10b ; store result
dec rsi ; next position
pop r10 ; restore rax
shr r10, 4 ; right shift by 4
loop .digit
pop rsi ; restore rsi (string address)
call Print
add rsp, 20
pop r10
ret
kernel/isr.asm
Now, let's examine some of our Interrupt Service Routines:
Τώρα, ας εξετάσουμε τις Ρουτίνες Εξυπηρέτησης Διακοπών (Interrupt Service Routines):
; INTERRUPT SERVICE ROUTINES
BITS 64
;---Initialized data----------------------------------------------------------
systimer_ticks dq 0
keyboard_scancode dq 0
error_code_low dw 0
error_code_high dw 0
int_message dw 17
db 'Interrupt raised!'
division_by_zero_message dw 17
db 'Division by zero!'
gpf_message dw 25
db 'General Protection Fault!'
pf_message dw 11
db 'Page Fault!'
;---Code------------------------------------------------------------------------
ISR_dummy:
;***************************************************************************;
; Just a dummy generic handler. It prints the message "Interrupt raised!". ;
;***************************************************************************;
cli
push rax
push r8
push r9
push rsi
mov ah, (VGA_COLOR_RED << 4) | VGA_COLOR_LIGHT_BROWN
mov r8, 1
mov r9, 1
mov rsi, int_message
Call Print
pop rsi
pop r9
pop r8
pop rax
.halt: hlt
jmp .halt ; Infinite loop
iretq
ISR_Division_by_Zero:
;***************************************************************************;
; Divizion by zero handler ;
;***************************************************************************;
cli
push rax
push r8
push r9
push rsi
mov ah, (VGA_COLOR_RED << 4) | VGA_COLOR_LIGHT_BROWN
mov r8, 1
mov r9, 1
mov rsi, division_by_zero_message
Call Print
pop rsi
pop r9
pop r8
pop rax
.halt: hlt
jmp .halt ; Infinite loop
iretq
ISR_GPF:
;***************************************************************************;
; General Protection Fault handler ;
;***************************************************************************;
cli
push rax
push r8
push r9
push rsi
mov ah, (VGA_COLOR_RED << 4) | VGA_COLOR_LIGHT_BROWN
mov r8, 1
mov r9, 1
mov rsi, gpf_message
Call Print
pop rsi
pop r9
pop r8
pop rax
.halt: hlt
jmp .halt ; Infinite loop
iretq
ISR_Page_Fault:
;***************************************************************************;
; Page Fault handler ;
;***************************************************************************;
cli
pop word [error_code_high]
pop word [error_code_low]
push rax
push r8
push r9
push rsi
mov ah, (VGA_COLOR_RED << 4) | VGA_COLOR_LIGHT_BROWN
mov r8, 1
mov r9, 1
mov rsi, pf_message
call Print
pop rsi
pop r9
pop r8
pop rax
.halt: hlt
jmp .halt ; Infinite loop
iretq
ISR_systimer:
;*****************************************************************************;
; System Timer Interrupt Service Routine (IRQ0 mapped to INT 0x20) ;
;*****************************************************************************;
push rax
inc qword [systimer_ticks]
mov al, PIC_EOI ; Send EOI (End of Interrupt) command
out PIC1_COMMAND, al
pop rax
iretq
ISR_keyboard:
;*****************************************************************************;
; Keyboard Controller Interrupt Service Routine (IRQ1 mapped to INT 0x21) ;
;*****************************************************************************;
push rax
xor rax, rax
in al, 0x60 ; MUST read byte from keyboard (else no more interrupts).
mov [keyboard_scancode], al
mov al, PIC_EOI ; Send EOI (End of Interrupt) command
out PIC1_COMMAND, al
pop rax
iretq
kernel/kernel.asm
Here, we added some code to see our Interrupt Service Routines in action:
Εδώ, προσθέσαμε κώδικα για να δούμε τις Ρουτίνες Εξυπηρέτησης Διακοπών σε δράση:
BITS 64 ; We have entered the long mode! :)
;---Define----------------------------------------------------------------------
%define DATA_SEG 0x0010
;---Initialized data------------------------------------------------------------
hello_world_message dw 12
db 'Hello World!'
;---Include---------------------------------------------------------------------
%include "kernel/idt.asm"
%include "kernel/isr.asm"
%include "kernel/video.asm"
;---Code------------------------------------------------------------------------
Kernel:
lidt [IDTR] ; Load our IDT
mov al, 0x80 ; OCW1: Unmask all interrupts at master PIC
out PIC1_DATA, al
mov al, 0x80 ; OCW1: Unmask all interrupts at master PIC
out PIC2_DATA, al
; Set all segments registers to DATA_SEG
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Clear the screen.
mov rax, 0x0020002000200020 ; Set background color to black (0) and
; character to blank space (20).
call Fill_screen
; Print "Hello World!" at the upper right corner
mov ah, 0x1E
mov r8, 69
mov r9, 1
mov rsi, hello_world_message
call Print
; Uncomment the following lines if you want to trigger a "Division by zero" exception.
; mov eax, 1
; mov ecx, 0
; div ecx
.loop:
; Print system timer ticks.
mov ah, VGA_COLOR_LIGHT_GREEN
mov r8, 1
mov r9, 2
mov r10, [systimer_ticks]
call Print_hex
; Print keyboard scan code.
mov r10, [keyboard_scancode]
mov ah, VGA_COLOR_LIGHT_CYAN
mov r8, 1
mov r9, 4
call Print_hex
jmp .loop ; Infinite loop.
Running our codeΕκτέλεση του κώδικά μας
We can test our code very easily using an emulator like QEMU:
Μπορούμε να δοκιμάσουμε τον κώδικά μας πολύ εύκολα, χρησιμοποιώντας έναν εξομοιωτή όπως ο QEMU:
$ make
$ qemu-system-x86_64 -drive format=raw,file=os.bin
(Press some keys to watch the scan code change.)
(Πατήστε μερικά πλήκτρα για να δείτε τον κωδικό του πλήκτρου να αλλάζει.)
Now, we have a way to measure time and we can get input from our keyboard! What a progress! :D
Τώρα, λοιπόν, έχουμε έναν τρόπο για να μετρήσουμε τον χρόνο και μπορούμε να πάρουμε είσοδο από το πληκτρολόγιό μας! Τι μεγάλη πρόοδος! :D
To see the "Division by zero" ISR in action, uncomment the relevant lines:
Για να δειτε την εξαίρεση της "Διαίρεση με το μηδέν" σε δράση, αφαιρέστε τα σχόλια από τις σχετικές γραμμές:
; Uncomment the following lines if you want to trigger a "Division by zero" exception.
mov eax, 1
mov ecx, 0
div ecx
Full codeΠλήρης κώδικας
You can download the full code from here: code
Μπορείτε να κατεβάσετε τον πλήρη κώδικα απο εδώ: κώδικας