In the lecture, we will talk about interrupts and see how to set up interrupt descriptor table as well as process interrupts.
Interrupt handling is especially important part of the operating system.
For example, If we don’t implement interrupt handling for keyboard, then we cannot use keyboard
for interacting with computer.
Process scheduling which is an essential part of our operating system relies on timer interrupt.
Before we delve into the details, let’s talk a little bit about interrupt and exception. When we look at interrupts,
we actually refer to hardware interrupts such as timer, keyboard interrupts etc. Whereas exception occurs
as a result of execution errors or internal processor errors.
The process of handling interrupts and exceptions are very similar.
As we have learned the general process of interrupt handling, in this section we will go into details
about how to set up idt and write interrupt service routines to handle specific interrupts.
When an interrupt is fired, the correspond idt entry is selected, the entry includes the information about
which code segment the interrupt handler is at and its attributes, the offset and other info, etc
.
So the essential data about the interrupt handler is stored in idt entry.
Let's take a look.
The idt entry takes up 16 bytes. The 64-bit offset of the interrupt handler is divided into three parts
.
As you see, the lower 16 bits is stored in the first two bytes.
then the second part and last part.
The bits 16 to 31 holds the selector of the code segment descriptor which the interrupt handler is at
.
IST stands for interrupt stack table. If we assign a value to this field, when the interrupt is fired,
the stack pointer this field references is loaded into rsp register.
We don't use the IST in this course.
So we simply set it to zero meaning that this field is not used.
As for the attributes, there is actually only one field we can modify which is DPL field.
The present bit, as we know, should be 1. And the value 01110 indicates that this entry
is interrupt descriptor table.
The dpl field specifies that which cpl can access this descriptor. For example, if we set DPL to 0,
then we cannot access the descriptor when we run in ring3 because the cpl is 3.
This field is useful when the descriptor is for the software interrupt. We will talk about that later in the course
.
As for the hardware interrupt, we simply set the DPL to 0. Because the processor ignores
the dpl of the descriptor of hardware interrupt and exceptions.
There are 256 different exceptions and interrupts vectors we can use.
As you can see, the numbers here are called interrupt vectors which identifies the exceptions and interrupts
.
For example, the vector of divide by zero exception is 0. 1 represents the debug exception and so on.
Note that the vectors from 0 to 31 are predefine by the processor.
We cannot redefine them. The vectors from 32 to 255 are user defined.
The vectors for hardware interrupts and software interrupts are within this range.
In this lecture, we will write a handler to deal with divide by zero exception.
So let's get started.
OK, in the kernel, we implement the handler for the divide by zero exception which is vector 0.
So first off, we define the idt which can have 256 entries in total.
So we define the label idt
and here we use directive repeat and then the specific times we want to repeat,
256 in this example.
Here we simply set all the entries with the same value which is 0.
As we did with gdt, we also define a constant
idt length
as well as idt pointer.
The structure of idt pointer is the same as gdt pointer.
The first two bytes stores the length of idt
and address of idt is stored in the next 8 bytes.
So for the moment, we set all the fields of the entries to zero and change the values of specific entries
in the code when they need to.
Because the attribute and selector value in each entry are the same in this example, we set these two fields here.
So the first field is attribute field.
As we have seen the structure of the entry in the slides, the attribute field is in the 6th byte.
The value we copy to the field is
8e
which is 10001110 in binary.
You can see present bit is 1 and 01110 means that this is the interrupt gate descriptor.
DPL is simply set to 0.
Another field is in 3rd byte.
In this example, we set it to
8 which is the same code segment descriptor that we currently use.
One thing worth mentioning is that when using interrupt gate, we can only jump to the code segment
that is in the same privilege or higher privilege than we are currently at.
For example, if the DPL of the code segment the selector 8 reference is 3,
then we cannot use this interrupt gate descriptor to jump to the handler because we are currently running in ring0,
since ring0 has higher privilege than ring3.
So you see using interrupt gate descriptor, we can only jump from lower privilege to higher privilege segment.
the dpl of the code segment descriptor the selector 8 reference is 0,
we can be sure that jump will succeed because ring0 is the most privileged level in our system.
Alright, back to the start of kernel.
In this example, we only want to write the handler for divide by zero exception which is the first exception
.
we copy the offset of the handler to the first idt entry.
we move rdi idt
And the move rax
handler0
Ok now rdi holds the address of idt and rax stores the offset of handler0.
As we have seen it before, the offset is divided into three parts in the idt entry.
The lower 16 bits is in the first two bytes.
we move rdi ax
which will copy the lower 16 bits of the offset to the location that rdi points to, that is
the first two bytes of idt entry.
Then we shift the offset in rax rightward by 16 bits.
And now the bit 16 to 31 are in the register ax.
we move
rdi + 6
ax.
What this instruction does is we copy the bit 16 to 31 of the offset to the second part
which is at the 7th byte in the entry.
We continue doing it. Shift the offset 16 bits again.
And bit 32 to 63 are in register eax now.
So we move.
rdi
+ 8
and eax which will copy the value in eax to the third part of the offset.
OK, we are done.
Let’s load idt pointer using instruction load idt
,
and the idt pointer
With the idt set up and loaded,
what we are going to do next is we are going to write the handler. In this example,
the handler is called Handler0, so we defines the label
handler0.
And it does nothing except print character D on the screen so that we know that divide by zero exception
is actually handled by this service routine.
So we copy the code of printing characters here.
and the character we want to print is D. To make the character slightly different, we set the attribute
of the character to the value c, which means the character is in red.
The return instruction is different than the normal return instruction ret in procedures.
Here we use interrupt return
which will pop more data than the regular return and can return to the different privilege level
.
We will use this instruction to jump to ring3 manually later in this section.
More details will be discussed there.
For the moment,
For now we just write it here to finish the handler. And also we stop the system by jumping to end.
The reason is that we have an exception in the privilege level 0 where the system kernel runs.
If our kernel has an error, we should stop it,
print the error message to let us know and fix the error.
OK, now, creating a divide by zero error is simple.
All we need to do is using div instruction.
So first off, we zero a register, for example.
rbx and then div rbx
If all things are set correctly, we will see character D in red is printed on screen.
we save the file
and run the script in the terminal.
run the bochs.
The red character D is printed on screen.