Interrupts Tutorial: Difference between revisions
[unchecked revision] | [unchecked revision] |
m (Bot: Replace deprecated source tag with syntaxhighlight) |
|||
(26 intermediate revisions by 8 users not shown) | |||
Line 1: | Line 1: | ||
{{Rating|1}} |
{{Rating|1}} |
||
[[Category:Tutorials]] |
|||
Every operating system that needs to work with the hardware must have a set of interrupts. First, you need to learn about interrupts and how they work. http://www.osdever.net/tutorials/view/irqs is a good resource to check. This tutorial will tell you how to set up and use interrupts. |
|||
Every operating system that needs to work with the hardware (efficiently) must make use of interrupts. For example, you could use the entirety of an AP to poll the mouse, or you could use the mouse IRQs instead and save much more CPU time, and a lot of electrical load. Therefore, every reasonable operating system makes use of interrupts. |
|||
==IDT table== |
|||
When an interrupt is fired, the CPU looks at the IDT table, and finds what method needs to be called. Here, we build the IDT table as an array, and load it using the function "lidt" |
|||
<source lang="c"> |
|||
struct IDT_entry{ |
|||
unsigned short int offset_lowerbits; |
|||
unsigned short int selector; |
|||
unsigned char zero; |
|||
unsigned char type_attr; |
|||
unsigned short int offset_higherbits; |
|||
}; |
|||
==Pre-requisites== |
|||
struct IDT_entry IDT[256]; |
|||
Before you create an IDT, you need to create a [[GDT]], load it properly, and configure the segment registers accordingly. |
|||
</source> |
|||
Now, we need to remap the PIC (Programmable Interrupt Controller) and fill the IDT with the correct values. |
|||
<source lang="c"> |
|||
void idt_init(void) { |
|||
extern int load_idt(); |
|||
extern int irq0(); |
|||
extern int irq1(); |
|||
extern int irq2(); |
|||
extern int irq3(); |
|||
extern int irq4(); |
|||
extern int irq5(); |
|||
extern int irq6(); |
|||
extern int irq7(); |
|||
extern int irq8(); |
|||
extern int irq9(); |
|||
extern int irq10(); |
|||
extern int irq11(); |
|||
extern int irq12(); |
|||
extern int irq13(); |
|||
extern int irq14(); |
|||
extern int irq15(); |
|||
==[[Interrupt Descriptor Table]]== |
|||
unsigned long irq0_address; |
|||
===Entries=== |
|||
unsigned long irq1_address; |
|||
In order to make use of interrupts, you need an IDT. |
|||
unsigned long irq2_address; |
|||
unsigned long irq3_address; |
|||
unsigned long irq4_address; |
|||
unsigned long irq5_address; |
|||
unsigned long irq6_address; |
|||
unsigned long irq7_address; |
|||
unsigned long irq8_address; |
|||
unsigned long irq9_address; |
|||
unsigned long irq10_address; |
|||
unsigned long irq11_address; |
|||
unsigned long irq12_address; |
|||
unsigned long irq13_address; |
|||
unsigned long irq14_address; |
|||
unsigned long irq15_address; |
|||
unsigned long idt_address; |
|||
unsigned long idt_ptr[2]; |
|||
When an interrupt is fired, the CPU uses the vector as an index into the IDT. The CPU reads the entry of the IDT in order to figure out what to do prior to calling the [[ISR]], and what the address of the handler is. |
|||
/* remapping the PIC */ |
|||
outb(0x20, 0x11); |
|||
outb(0xA0, 0x11); |
|||
outb(0x21, 0x20); |
|||
outb(0xA1, 40); |
|||
outb(0x21, 0x04); |
|||
outb(0xA1, 0x02); |
|||
outb(0x21, 0x01); |
|||
outb(0xA1, 0x01); |
|||
outb(0x21, 0x0); |
|||
outb(0xA1, 0x0); |
|||
This is the structure of a single (32-bit) IDT entry: |
|||
irq0_address = (unsigned long)irq0; |
|||
<syntaxhighlight lang="c"> |
|||
IDT[32].offset_lowerbits = irq0_address & 0xffff; |
|||
typedef struct { |
|||
IDT[32].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
uint16_t isr_low; // The lower 16 bits of the ISR's address |
|||
IDT[32].zero = 0; |
|||
uint16_t kernel_cs; // The GDT segment selector that the CPU will load into CS before calling the ISR |
|||
IDT[32].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
uint8_t reserved; // Set to zero |
|||
IDT[32].offset_higherbits = (irq0_address & 0xffff0000) >> 16; |
|||
uint8_t attributes; // Type and attributes; see the IDT page |
|||
uint16_t isr_high; // The higher 16 bits of the ISR's address |
|||
} __attribute__((packed)) idt_entry_t; |
|||
</syntaxhighlight> |
|||
and this is the structure of a single (64-bit) IDT entry: |
|||
<syntaxhighlight lang="c"> |
|||
typedef struct { |
|||
uint16_t isr_low; // The lower 16 bits of the ISR's address |
|||
uint16_t kernel_cs; // The GDT segment selector that the CPU will load into CS before calling the ISR |
|||
uint8_t ist; // The IST in the TSS that the CPU will load into RSP; set to zero for now |
|||
uint8_t attributes; // Type and attributes; see the IDT page |
|||
uint16_t isr_mid; // The higher 16 bits of the lower 32 bits of the ISR's address |
|||
uint32_t isr_high; // The higher 32 bits of the ISR's address |
|||
uint32_t reserved; // Set to zero |
|||
} __attribute__((packed)) idt_entry_t; |
|||
</syntaxhighlight> |
|||
===Table=== |
|||
To create an IDT, simply create a 256-entry array of descriptors: |
|||
<syntaxhighlight lang="c"> |
|||
__attribute__((aligned(0x10))) |
|||
static idt_entry_t idt[256]; // Create an array of IDT entries; aligned for performance |
|||
</syntaxhighlight> |
|||
You will also need a special IDTR structure, which looks like: |
|||
<syntaxhighlight lang="c"> |
|||
typedef struct { |
|||
uint16_t limit; |
|||
uint32_t base; |
|||
} __attribute__((packed)) idtr_t; |
|||
</syntaxhighlight> |
|||
for a 32-bit IDT, or like: |
|||
<syntaxhighlight lang="c"> |
|||
typedef struct { |
|||
uint16_t limit; |
|||
uint64_t base; |
|||
} __attribute__((packed)) idtr_t; |
|||
</syntaxhighlight> |
|||
for a 64-bit IDT. |
|||
Don't forget to define an IDTR: |
|||
irq1_address = (unsigned long)irq1; |
|||
<syntaxhighlight lang="c"> |
|||
IDT[33].offset_lowerbits = irq1_address & 0xffff; |
|||
static idtr_t idtr; |
|||
IDT[33].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
</syntaxhighlight> |
|||
IDT[33].zero = 0; |
|||
IDT[33].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[33].offset_higherbits = (irq1_address & 0xffff0000) >> 16; |
|||
irq2_address = (unsigned long)irq2; |
|||
IDT[34].offset_lowerbits = irq2_address & 0xffff; |
|||
IDT[34].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[34].zero = 0; |
|||
IDT[34].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[34].offset_higherbits = (irq2_address & 0xffff0000) >> 16; |
|||
irq3_address = (unsigned long)irq3; |
|||
IDT[35].offset_lowerbits = irq3_address & 0xffff; |
|||
IDT[35].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[35].zero = 0; |
|||
IDT[35].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[35].offset_higherbits = (irq3_address & 0xffff0000) >> 16; |
|||
irq4_address = (unsigned long)irq4; |
|||
IDT[36].offset_lowerbits = irq4_address & 0xffff; |
|||
IDT[36].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[36].zero = 0; |
|||
IDT[36].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[36].offset_higherbits = (irq4_address & 0xffff0000) >> 16; |
|||
irq5_address = (unsigned long)irq5; |
|||
IDT[37].offset_lowerbits = irq5_address & 0xffff; |
|||
IDT[37].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[37].zero = 0; |
|||
IDT[37].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[37].offset_higherbits = (irq5_address & 0xffff0000) >> 16; |
|||
irq6_address = (unsigned long)irq6; |
|||
IDT[38].offset_lowerbits = irq6_address & 0xffff; |
|||
IDT[38].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[38].zero = 0; |
|||
IDT[38].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[38].offset_higherbits = (irq6_address & 0xffff0000) >> 16; |
|||
irq7_address = (unsigned long)irq7; |
|||
IDT[39].offset_lowerbits = irq7_address & 0xffff; |
|||
IDT[39].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[39].zero = 0; |
|||
IDT[39].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[39].offset_higherbits = (irq7_address & 0xffff0000) >> 16; |
|||
irq8_address = (unsigned long)irq8; |
|||
IDT[40].offset_lowerbits = irq8_address & 0xffff; |
|||
IDT[40].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[40].zero = 0; |
|||
IDT[40].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[40].offset_higherbits = (irq8_address & 0xffff0000) >> 16; |
|||
irq9_address = (unsigned long)irq9; |
|||
IDT[41].offset_lowerbits = irq9_address & 0xffff; |
|||
IDT[41].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[41].zero = 0; |
|||
IDT[41].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[41].offset_higherbits = (irq9_address & 0xffff0000) >> 16; |
|||
irq10_address = (unsigned long)irq10; |
|||
IDT[42].offset_lowerbits = irq10_address & 0xffff; |
|||
IDT[42].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[42].zero = 0; |
|||
IDT[42].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[42].offset_higherbits = (irq10_address & 0xffff0000) >> 16; |
|||
irq11_address = (unsigned long)irq11; |
|||
IDT[43].offset_lowerbits = irq11_address & 0xffff; |
|||
IDT[43].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[43].zero = 0; |
|||
IDT[43].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[43].offset_higherbits = (irq11_address & 0xffff0000) >> 16; |
|||
irq12_address = (unsigned long)irq12; |
|||
IDT[44].offset_lowerbits = irq12_address & 0xffff; |
|||
IDT[44].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[44].zero = 0; |
|||
IDT[44].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[44].offset_higherbits = (irq12_address & 0xffff0000) >> 16; |
|||
irq13_address = (unsigned long)irq13; |
|||
IDT[45].offset_lowerbits = irq13_address & 0xffff; |
|||
IDT[45].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[45].zero = 0; |
|||
IDT[45].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[45].offset_higherbits = (irq13_address & 0xffff0000) >> 16; |
|||
irq14_address = (unsigned long)irq14; |
|||
IDT[46].offset_lowerbits = irq14_address & 0xffff; |
|||
IDT[46].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[46].zero = 0; |
|||
IDT[46].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[46].offset_higherbits = (irq14_address & 0xffff0000) >> 16; |
|||
irq15_address = (unsigned long)irq15; |
|||
IDT[47].offset_lowerbits = irq15_address & 0xffff; |
|||
IDT[47].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ |
|||
IDT[47].zero = 0; |
|||
IDT[47].type_attr = 0x8e; /* INTERRUPT_GATE */ |
|||
IDT[47].offset_higherbits = (irq15_address & 0xffff0000) >> 16; |
|||
/* fill the IDT descriptor */ |
|||
idt_address = (unsigned long)IDT ; |
|||
idt_ptr[0] = (sizeof (struct IDT_entry) * 256) + ((idt_address & 0xffff) << 16); |
|||
idt_ptr[1] = idt_address >> 16 ; |
|||
load_idt(idt_ptr); |
|||
===ISRs=== |
|||
In a C source file, define a general exception handler: |
|||
<syntaxhighlight lang="c"> |
|||
__attribute__((noreturn)) |
|||
void exception_handler(void); |
|||
void exception_handler() { |
|||
__asm__ volatile ("cli; hlt"); // Completely hangs the computer |
|||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
This will act as your main exception handler. When you receive a CPU exception, this is the handler you will call. |
|||
Here are the IRQ handlers in assembly |
|||
<source lang="c"> |
|||
global irq0 |
|||
global irq1 |
|||
global irq2 |
|||
global irq3 |
|||
global irq4 |
|||
global irq5 |
|||
global irq6 |
|||
global irq7 |
|||
global irq8 |
|||
global irq9 |
|||
global irq10 |
|||
global irq11 |
|||
global irq12 |
|||
global irq13 |
|||
global irq14 |
|||
global irq15 |
|||
Now, in an assembly file ('''nasm''' '''assembler''' specifically), define these two macros: |
|||
global load_idt |
|||
<syntaxhighlight lang="asm"> |
|||
%macro isr_err_stub 1 |
|||
isr_stub_%+%1: |
|||
call exception_handler |
|||
iret |
|||
%endmacro |
|||
; if writing for 64-bit, use iretq instead |
|||
%macro isr_no_err_stub 1 |
|||
isr_stub_%+%1: |
|||
call exception_handler |
|||
iret |
|||
%endmacro |
|||
</syntaxhighlight> |
|||
Then, use these macros to define your 32 exception handlers: |
|||
<syntaxhighlight lang="asm"> |
|||
extern exception_handler |
|||
isr_no_err_stub 0 |
|||
isr_no_err_stub 1 |
|||
isr_no_err_stub 2 |
|||
isr_no_err_stub 3 |
|||
isr_no_err_stub 4 |
|||
isr_no_err_stub 5 |
|||
isr_no_err_stub 6 |
|||
isr_no_err_stub 7 |
|||
isr_err_stub 8 |
|||
isr_no_err_stub 9 |
|||
isr_err_stub 10 |
|||
isr_err_stub 11 |
|||
isr_err_stub 12 |
|||
isr_err_stub 13 |
|||
isr_err_stub 14 |
|||
isr_no_err_stub 15 |
|||
isr_no_err_stub 16 |
|||
isr_err_stub 17 |
|||
isr_no_err_stub 18 |
|||
isr_no_err_stub 19 |
|||
isr_no_err_stub 20 |
|||
isr_no_err_stub 21 |
|||
isr_no_err_stub 22 |
|||
isr_no_err_stub 23 |
|||
isr_no_err_stub 24 |
|||
isr_no_err_stub 25 |
|||
isr_no_err_stub 26 |
|||
isr_no_err_stub 27 |
|||
isr_no_err_stub 28 |
|||
isr_no_err_stub 29 |
|||
isr_err_stub 30 |
|||
isr_no_err_stub 31 |
|||
</syntaxhighlight> |
|||
Finally, in assembly, define a "stub table". (This is used to prevent excessive code reuse, and not related to actual function.) |
|||
Using NASM macros: |
|||
global irq0_handler |
|||
<syntaxhighlight lang="asm"> |
|||
global irq1_handler |
|||
global |
global isr_stub_table |
||
isr_stub_table: |
|||
global irq3_handler |
|||
%assign i 0 |
|||
global irq4_handler |
|||
%rep 32 |
|||
global irq5_handler |
|||
dd isr_stub_%+i ; use DQ instead if targeting 64-bit |
|||
global irq6_handler |
|||
%assign i i+1 |
|||
global irq7_handler |
|||
%endrep |
|||
global irq8_handler |
|||
</syntaxhighlight> |
|||
global irq9_handler |
|||
===Assembling=== |
|||
global irq10_handler |
|||
Finally, we can assemble the IDT: |
|||
global irq11_handler |
|||
We need to |
|||
global irq12_handler |
|||
* 1. Assign the IDT entries with the correct values, |
|||
global irq13_handler |
|||
* 2. Reload the IDTR register, |
|||
global irq14_handler |
|||
* 3. Set the interrupt flag. |
|||
global irq15_handler |
|||
To define the entries, it is appropriate to make use of a helper function. That helper function would look like: |
|||
extern irq0_handler |
|||
<syntaxhighlight lang="c"> |
|||
extern irq1_handler |
|||
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags); |
|||
extern irq2_handler |
|||
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) { |
|||
extern irq3_handler |
|||
idt_entry_t* descriptor = &idt[vector]; |
|||
extern irq4_handler |
|||
extern irq5_handler |
|||
extern irq6_handler |
|||
extern irq7_handler |
|||
extern irq8_handler |
|||
extern irq9_handler |
|||
extern irq10_handler |
|||
extern irq11_handler |
|||
extern irq12_handler |
|||
extern irq13_handler |
|||
extern irq14_handler |
|||
extern irq15_handler |
|||
descriptor->isr_low = (uint32_t)isr & 0xFFFF; |
|||
irq0: |
|||
descriptor->kernel_cs = 0x08; // this value can be whatever offset your kernel code selector is in your GDT |
|||
pusha |
|||
descriptor->attributes = flags; |
|||
call irq0_handler |
|||
descriptor->isr_high = (uint32_t)isr >> 16; |
|||
popa |
|||
descriptor->reserved = 0; |
|||
iret |
|||
irq1: |
|||
pusha |
|||
call irq1_handler |
|||
popa |
|||
iret |
|||
irq2: |
|||
pusha |
|||
call irq2_handler |
|||
popa |
|||
iret |
|||
irq3: |
|||
pusha |
|||
call irq3_handler |
|||
popa |
|||
iret |
|||
irq4: |
|||
pusha |
|||
call irq4_handler |
|||
popa |
|||
iret |
|||
irq5: |
|||
pusha |
|||
call irq5_handler |
|||
popa |
|||
iret |
|||
irq6: |
|||
pusha |
|||
call irq6_handler |
|||
popa |
|||
iret |
|||
irq7: |
|||
pusha |
|||
call irq7_handler |
|||
popa |
|||
iret |
|||
irq8: |
|||
pusha |
|||
call irq8_handler |
|||
popa |
|||
iret |
|||
irq9: |
|||
pusha |
|||
call irq9_handler |
|||
popa |
|||
iret |
|||
irq10: |
|||
pusha |
|||
call irq10_handler |
|||
popa |
|||
iret |
|||
irq11: |
|||
pusha |
|||
call irq11_handler |
|||
popa |
|||
iret |
|||
irq12: |
|||
pusha |
|||
call irq12_handler |
|||
popa |
|||
iret |
|||
irq13: |
|||
pusha |
|||
call irq13_handler |
|||
popa |
|||
iret |
|||
irq14: |
|||
pusha |
|||
call irq14_handler |
|||
popa |
|||
iret |
|||
irq15: |
|||
pusha |
|||
call irq15_handler |
|||
popa |
|||
iret |
|||
load_idt: |
|||
mov edx, [esp + 4] |
|||
lidt [edx] |
|||
sti |
|||
ret |
|||
</source> |
|||
And here are the IRQ handlers in C, which are called from the assembly code. We must send the EOI (End of Interrupt) signal to the PIC, to tell it that we're done handling the interrupt. If we dont, the PIC will think that we're still busy, and won't send any more IRQs. |
|||
<source lang="c"> |
|||
void irq0_handler(void) { |
|||
outb(0x20, 0x20); //EOI |
|||
} |
} |
||
</syntaxhighlight> |
|||
for a 32-bit IDT, or like: |
|||
<syntaxhighlight lang="c"> |
|||
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags); |
|||
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) { |
|||
idt_entry_t* descriptor = &idt[vector]; |
|||
descriptor->isr_low = (uint64_t)isr & 0xFFFF; |
|||
void irq1_handler(void) { |
|||
descriptor->kernel_cs = GDT_OFFSET_KERNEL_CODE; |
|||
outb(0x20, 0x20); //EOI |
|||
descriptor->ist = 0; |
|||
descriptor->attributes = flags; |
|||
descriptor->isr_mid = ((uint64_t)isr >> 16) & 0xFFFF; |
|||
descriptor->isr_high = ((uint64_t)isr >> 32) & 0xFFFFFFFF; |
|||
descriptor->reserved = 0; |
|||
} |
} |
||
</syntaxhighlight> |
|||
for a 64-bit IDT. |
|||
Finally, to set the entries at last, this is what the function would look like: |
|||
void irq2_handler(void) { |
|||
<syntaxhighlight lang="c"> |
|||
outb(0x20, 0x20); //EOI |
|||
static bool vectors[IDT_MAX_DESCRIPTORS]; |
|||
} |
|||
extern void* isr_stub_table[]; |
|||
void irq3_handler(void) { |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void |
void idt_init(void); |
||
void idt_init() { |
|||
outb(0x20, 0x20); //EOI |
|||
idtr.base = (uintptr_t)&idt[0]; |
|||
} |
|||
idtr.limit = (uint16_t)sizeof(idt_entry_t) * IDT_MAX_DESCRIPTORS - 1; |
|||
for (uint8_t vector = 0; vector < 32; vector++) { |
|||
void irq5_handler(void) { |
|||
idt_set_descriptor(vector, isr_stub_table[vector], 0x8E); |
|||
outb(0x20, 0x20); //EOI |
|||
vectors[vector] = true; |
|||
} |
|||
} |
|||
__asm__ volatile ("lidt %0" : : "m"(idtr)); // load the new IDT |
|||
void irq6_handler(void) { |
|||
__asm__ volatile ("sti"); // set the interrupt flag |
|||
outb(0x20, 0x20); //EOI |
|||
} |
} |
||
</syntaxhighlight> |
|||
(IDT_MAX_DESCRIPTORS being the number of entries in your IDT - or the last entry's index + 1) |
|||
Congratulations! You have successfully defined your IDT, loaded it, and enabled interrupts. |
|||
void irq7_handler(void) { |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
==What to do next== |
|||
void irq8_handler(void) { |
|||
After completing this tutorial, there is still much left for you to do to fully harness the power of interrupts. |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq9_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq10_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq11_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq12_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq13_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq14_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
void irq15_handler(void) { |
|||
outb(0xA0, 0x20); |
|||
outb(0x20, 0x20); //EOI |
|||
} |
|||
</source> |
|||
Now you can write your handler methods! |
|||
==Interrupts in GRUB== |
|||
If you use GRUB as your bootloader, after setting up the IDT emulators, you will get a fatal error. This is most likely due to the fact that you haven't set up the GDT. Here is some the assembly code to example how you can set it. You should set the GDT before IDT, best in bootloader. |
|||
<source lang="asm"> |
|||
jmp load_gdt |
|||
;global descriptor table |
|||
gdt: |
|||
gdt_null: |
|||
dq 0 |
|||
gdt_code: |
|||
dw 0FFFFh |
|||
dw 0 |
|||
db 0 |
|||
db 10011010b |
|||
db 11001111b |
|||
db 0 |
|||
gdt_data: |
|||
dw 0FFFFh |
|||
dw 0 |
|||
db 0 |
|||
db 10010010b |
|||
db 11001111b |
|||
db 0 |
|||
gdt_end: |
|||
gdt_desc: |
|||
dw gdt_end - gdt - 1 |
|||
dd gdt |
|||
;load gdt |
|||
load_gdt: |
|||
cli ;disable interrupts |
|||
lgdt [gdt_desc] ;load GDT |
|||
sti ;enable interrupts |
|||
</source> |
|||
I have tested this code and it works in both Virtualbox and QEMU. Good luck! |
|||
Klakap |
|||
You can: |
|||
* Initialize the [[PIC]] |
|||
* Make use of PIC [[IRQ]]s |
|||
* Understand the [[NMI]] |
|||
* Configure the local [[APIC]] |
|||
* Write a driver for the [[IOAPIC]] |
|||
* Make use of [[Message Signaled Interrupts]] |
|||
==See also== |
==See also== |
||
===Threads=== |
===Threads=== |
||
* [https://forum.osdev.org/viewtopic.php?f=1&t=33160&p=285871#p285871 Interrupts |
* [https://forum.osdev.org/viewtopic.php?f=1&t=33160&p=285871#p285871 Interrupts don't work in GRUB] |
||
===References=== |
===References=== |
||
* [https://github.com/austanss/skylight/blob/trunk/kernel/src/cpu/interrupts/idt.c The code this tutorial is based off of] |
|||
* [https://arjunsreedharan.org/post/99370248137/kernel-201-lets-write-a-kernel-with-keyboard Kernel 201] |
|||
[[Category:Interrupts]] |
Latest revision as of 05:35, 9 June 2024
Difficulty level |
---|
![]() Beginner |
Every operating system that needs to work with the hardware (efficiently) must make use of interrupts. For example, you could use the entirety of an AP to poll the mouse, or you could use the mouse IRQs instead and save much more CPU time, and a lot of electrical load. Therefore, every reasonable operating system makes use of interrupts.
Pre-requisites
Before you create an IDT, you need to create a GDT, load it properly, and configure the segment registers accordingly.
Interrupt Descriptor Table
Entries
In order to make use of interrupts, you need an IDT.
When an interrupt is fired, the CPU uses the vector as an index into the IDT. The CPU reads the entry of the IDT in order to figure out what to do prior to calling the ISR, and what the address of the handler is.
This is the structure of a single (32-bit) IDT entry:
typedef struct {
uint16_t isr_low; // The lower 16 bits of the ISR's address
uint16_t kernel_cs; // The GDT segment selector that the CPU will load into CS before calling the ISR
uint8_t reserved; // Set to zero
uint8_t attributes; // Type and attributes; see the IDT page
uint16_t isr_high; // The higher 16 bits of the ISR's address
} __attribute__((packed)) idt_entry_t;
and this is the structure of a single (64-bit) IDT entry:
typedef struct {
uint16_t isr_low; // The lower 16 bits of the ISR's address
uint16_t kernel_cs; // The GDT segment selector that the CPU will load into CS before calling the ISR
uint8_t ist; // The IST in the TSS that the CPU will load into RSP; set to zero for now
uint8_t attributes; // Type and attributes; see the IDT page
uint16_t isr_mid; // The higher 16 bits of the lower 32 bits of the ISR's address
uint32_t isr_high; // The higher 32 bits of the ISR's address
uint32_t reserved; // Set to zero
} __attribute__((packed)) idt_entry_t;
Table
To create an IDT, simply create a 256-entry array of descriptors:
__attribute__((aligned(0x10)))
static idt_entry_t idt[256]; // Create an array of IDT entries; aligned for performance
You will also need a special IDTR structure, which looks like:
typedef struct {
uint16_t limit;
uint32_t base;
} __attribute__((packed)) idtr_t;
for a 32-bit IDT, or like:
typedef struct {
uint16_t limit;
uint64_t base;
} __attribute__((packed)) idtr_t;
for a 64-bit IDT.
Don't forget to define an IDTR:
static idtr_t idtr;
ISRs
In a C source file, define a general exception handler:
__attribute__((noreturn))
void exception_handler(void);
void exception_handler() {
__asm__ volatile ("cli; hlt"); // Completely hangs the computer
}
This will act as your main exception handler. When you receive a CPU exception, this is the handler you will call.
Now, in an assembly file (nasm assembler specifically), define these two macros:
%macro isr_err_stub 1
isr_stub_%+%1:
call exception_handler
iret
%endmacro
; if writing for 64-bit, use iretq instead
%macro isr_no_err_stub 1
isr_stub_%+%1:
call exception_handler
iret
%endmacro
Then, use these macros to define your 32 exception handlers:
extern exception_handler
isr_no_err_stub 0
isr_no_err_stub 1
isr_no_err_stub 2
isr_no_err_stub 3
isr_no_err_stub 4
isr_no_err_stub 5
isr_no_err_stub 6
isr_no_err_stub 7
isr_err_stub 8
isr_no_err_stub 9
isr_err_stub 10
isr_err_stub 11
isr_err_stub 12
isr_err_stub 13
isr_err_stub 14
isr_no_err_stub 15
isr_no_err_stub 16
isr_err_stub 17
isr_no_err_stub 18
isr_no_err_stub 19
isr_no_err_stub 20
isr_no_err_stub 21
isr_no_err_stub 22
isr_no_err_stub 23
isr_no_err_stub 24
isr_no_err_stub 25
isr_no_err_stub 26
isr_no_err_stub 27
isr_no_err_stub 28
isr_no_err_stub 29
isr_err_stub 30
isr_no_err_stub 31
Finally, in assembly, define a "stub table". (This is used to prevent excessive code reuse, and not related to actual function.)
Using NASM macros:
global isr_stub_table
isr_stub_table:
%assign i 0
%rep 32
dd isr_stub_%+i ; use DQ instead if targeting 64-bit
%assign i i+1
%endrep
Assembling
Finally, we can assemble the IDT: We need to
- 1. Assign the IDT entries with the correct values,
- 2. Reload the IDTR register,
- 3. Set the interrupt flag.
To define the entries, it is appropriate to make use of a helper function. That helper function would look like:
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags);
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
idt_entry_t* descriptor = &idt[vector];
descriptor->isr_low = (uint32_t)isr & 0xFFFF;
descriptor->kernel_cs = 0x08; // this value can be whatever offset your kernel code selector is in your GDT
descriptor->attributes = flags;
descriptor->isr_high = (uint32_t)isr >> 16;
descriptor->reserved = 0;
}
for a 32-bit IDT, or like:
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags);
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
idt_entry_t* descriptor = &idt[vector];
descriptor->isr_low = (uint64_t)isr & 0xFFFF;
descriptor->kernel_cs = GDT_OFFSET_KERNEL_CODE;
descriptor->ist = 0;
descriptor->attributes = flags;
descriptor->isr_mid = ((uint64_t)isr >> 16) & 0xFFFF;
descriptor->isr_high = ((uint64_t)isr >> 32) & 0xFFFFFFFF;
descriptor->reserved = 0;
}
for a 64-bit IDT.
Finally, to set the entries at last, this is what the function would look like:
static bool vectors[IDT_MAX_DESCRIPTORS];
extern void* isr_stub_table[];
void idt_init(void);
void idt_init() {
idtr.base = (uintptr_t)&idt[0];
idtr.limit = (uint16_t)sizeof(idt_entry_t) * IDT_MAX_DESCRIPTORS - 1;
for (uint8_t vector = 0; vector < 32; vector++) {
idt_set_descriptor(vector, isr_stub_table[vector], 0x8E);
vectors[vector] = true;
}
__asm__ volatile ("lidt %0" : : "m"(idtr)); // load the new IDT
__asm__ volatile ("sti"); // set the interrupt flag
}
(IDT_MAX_DESCRIPTORS being the number of entries in your IDT - or the last entry's index + 1)
Congratulations! You have successfully defined your IDT, loaded it, and enabled interrupts.
What to do next
After completing this tutorial, there is still much left for you to do to fully harness the power of interrupts.
You can:
- Initialize the PIC
- Make use of PIC IRQs
- Understand the NMI
- Configure the local APIC
- Write a driver for the IOAPIC
- Make use of Message Signaled Interrupts