Interrupts Tutorial: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
m (Bot: Replace deprecated source tag with syntaxhighlight)
 
(One intermediate revision by the same user not shown)
Line 14: Line 14:


This is the structure of a single (32-bit) IDT entry:
This is the structure of a single (32-bit) IDT entry:
<source lang="c">
<syntaxhighlight lang="c">
typedef struct {
typedef struct {
uint16_t isr_low; // The lower 16 bits of the ISR's address
uint16_t isr_low; // The lower 16 bits of the ISR's address
Line 22: Line 22:
uint16_t isr_high; // The higher 16 bits of the ISR's address
uint16_t isr_high; // The higher 16 bits of the ISR's address
} __attribute__((packed)) idt_entry_t;
} __attribute__((packed)) idt_entry_t;
</syntaxhighlight>
</source>
and this is the structure of a single (64-bit) IDT entry:
and this is the structure of a single (64-bit) IDT entry:
<source lang="c">
<syntaxhighlight lang="c">
typedef struct {
typedef struct {
uint16_t isr_low; // The lower 16 bits of the ISR's address
uint16_t isr_low; // The lower 16 bits of the ISR's address
Line 34: Line 34:
uint32_t reserved; // Set to zero
uint32_t reserved; // Set to zero
} __attribute__((packed)) idt_entry_t;
} __attribute__((packed)) idt_entry_t;
</syntaxhighlight>
</source>
===Table===
===Table===
To create an IDT, simply create a 256-entry array of descriptors:
To create an IDT, simply create a 256-entry array of descriptors:
<source lang="c">
<syntaxhighlight lang="c">
__attribute__((aligned(0x10)))
__attribute__((aligned(0x10)))
static idt_entry_t idt[256]; // Create an array of IDT entries; aligned for performance
static idt_entry_t idt[256]; // Create an array of IDT entries; aligned for performance
</syntaxhighlight>
</source>
You will also need a special IDTR structure, which looks like:
You will also need a special IDTR structure, which looks like:
<source lang="c">
<syntaxhighlight lang="c">
typedef struct {
typedef struct {
uint16_t limit;
uint16_t limit;
uint32_t base;
uint32_t base;
} __attribute__((packed)) idtr_t;
} __attribute__((packed)) idtr_t;
</syntaxhighlight>
</source>
for a 32-bit IDT, or like:
for a 32-bit IDT, or like:
<source lang="c">
<syntaxhighlight lang="c">
typedef struct {
typedef struct {
uint16_t limit;
uint16_t limit;
uint64_t base;
uint64_t base;
} __attribute__((packed)) idtr_t;
} __attribute__((packed)) idtr_t;
</syntaxhighlight>
</source>
for a 64-bit IDT.
for a 64-bit IDT.


Don't forget to define an IDTR:
Don't forget to define an IDTR:
<source lang="c">
<syntaxhighlight lang="c">
static idtr_t idtr;
static idtr_t idtr;
</syntaxhighlight>
</source>


===ISRs===
===ISRs===
In a C source file, define a general exception handler:
In a C source file, define a general exception handler:
<source lang="c">
<syntaxhighlight lang="c">
__attribute__((noreturn))
__attribute__((noreturn))
void exception_handler(void);
void exception_handler(void);
Line 70: Line 70:
__asm__ volatile ("cli; hlt"); // Completely hangs the computer
__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.
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:
Now, in an assembly file ('''nasm''' '''assembler''' specifically), define these two macros:
<source lang="asm">
<syntaxhighlight lang="asm">
%macro isr_err_stub 1
%macro isr_err_stub 1
isr_stub_%+%1:
isr_stub_%+%1:
Line 86: Line 86:
iret
iret
%endmacro
%endmacro
</syntaxhighlight>
</source>
Then, use these macros to define your 32 exception handlers:
Then, use these macros to define your 32 exception handlers:
<source lang="asm">
<syntaxhighlight lang="asm">
extern exception_handler
extern exception_handler
isr_no_err_stub 0
isr_no_err_stub 0
Line 122: Line 122:
isr_err_stub 30
isr_err_stub 30
isr_no_err_stub 31
isr_no_err_stub 31
</syntaxhighlight>
</source>
Finally, in assembly, define a "stub table". (This is used to prevent excessive code reuse, and not related to actual function.)
Finally, in assembly, define a "stub table". (This is used to prevent excessive code reuse, and not related to actual function.)


Using NASM macros:
Using NASM macros:
<source lang="asm">
<syntaxhighlight lang="asm">
global isr_stub_table
global isr_stub_table
isr_stub_table:
isr_stub_table:
Line 134: Line 134:
%assign i i+1
%assign i i+1
%endrep
%endrep
</syntaxhighlight>
</source>
===Assembling===
===Assembling===
Finally, we can assemble the IDT:
Finally, we can assemble the IDT:
Line 143: Line 143:


To define the entries, it is appropriate to make use of a helper function. That helper function would look like:
To define the entries, it is appropriate to make use of a helper function. That helper function would look like:
<source lang="c">
<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);
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
Line 154: Line 154:
descriptor->reserved = 0;
descriptor->reserved = 0;
}
}
</syntaxhighlight>
</source>
for a 32-bit IDT, or like:
for a 32-bit IDT, or like:
<source lang="c">
<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);
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
void idt_set_descriptor(uint8_t vector, void* isr, uint8_t flags) {
Line 169: Line 169:
descriptor->reserved = 0;
descriptor->reserved = 0;
}
}
</syntaxhighlight>
</source>
for a 64-bit IDT.
for a 64-bit IDT.


Finally, to set the entries at last, this is what the function would look like:
Finally, to set the entries at last, this is what the function would look like:
<source lang="c">
<syntaxhighlight lang="c">
static bool vectors[IDT_MAX_DESCRIPTORS];
static bool vectors[IDT_MAX_DESCRIPTORS];


Line 191: Line 191:
__asm__ volatile ("sti"); // set the interrupt flag
__asm__ volatile ("sti"); // set the interrupt flag
}
}
</syntaxhighlight>
</source>
(IDT_MAX_DESCRIPTORS being the number of entries in your IDT - or the last entry's index + 1)
(IDT_MAX_DESCRIPTORS being the number of entries in your IDT - or the last entry's index + 1)



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:

See also

Threads

References