Hello,
in this lecture, we will create a new file to process the interrupts. Before we delve into the details,
we need to discuss a little bit about calling conventions.
In this course, we use system v amd64 calling convention where the first six arguments are stored
in rdi rsi rdx rcx r8 and r9.
Other parameters are passed on the stack in reverse order.
The return value is stored in rax register.
rax, rcx, rdx, rsi and so on are called caller saved registers
which means the caller has to save the value of these registers if it calls other function which could alter these registers.
Normally the caller will save the value on the stack before it calls other functions
and restore the value of the registers after the functions return.
Other registers, such as rbx, rbp, r12, etc, are called callee saved registers
which means the value of these registers are preserved when we call a function and return from it.
OK, let's get started.
Because we need the interrupt structures when we deal with the interrupts in c file. We created a header file
trap.h to define these structures.
in the header file, the first thing we are going to do is add a guard so that the header file is included only once.
What it does is that if this label is not defined then define it and the next time the file gets included,
the whole block of code will be skipped beacuse the label is defined.
Next we include stdint file because we will define the structures using fixed width data types.
The first structure we define is idt entry. As we have learned in the previous lectures,
the first two bytes stores lower 16 bits of offset.
So the data type we use is unsigned integer which is 16 bits.
The selector is stored in the next two bytes.
We use the same data type. Then we have reserved field and attribute, both of which are 8bits.
next two fields are the mid and upper bits of the offset. one is 16bits and another is 32bits.
The last 32bits are reserved.
Ok the next structure is idt pointer which is used for load idt instruction.
The first two bytes are the limit and the next 8 bytes are the address of the IDT.
Note that here we add the option attribute packed so that the structure is stored without padding in it.
Otherwise the data is stored to the natural alignment and we will have padding between these two items in this case
.
As a result, the load idt instruction
will load the register with wrong data. So be sure to add the packed attribute.
Ok, before we finish this file, we need another assembly file also called trap.
Remember we have removed all the interrupt related code in the kernel file in the last video.
They will be added in this file. As you can see,
we declare those handlers globally so that we can reference them in the c file.
The vector0 handles divide by zero exception, vector1 is for debug exception and so on.
Some of vectors are reserved such as vector 9, vector 15.
All these interrupts are processed in the same function called handler in c file.
Since it’s not defined in this file, we use extern which means the function is not defined here
but in other modules.
The end of interrupt, read isr and load idt
are the functions also used in the c file.
OK, we start off with vector0.
We pushed two value on the stack. The first one is error code.
Some of the exceptions such as general fault exception have error code pushed on the stack by the processor before the handler executes.
But divide by zero exception doesn’t have error code,
so we push 0 by ourselves acting as a placeholder.
The second value pushed on the stack is the index value so that we know which exception or interrupt is actually called.
We push 0 which means this is the first exception and jump to trap.
The reason we push them on the stack is that we will handler all the exceptions and interrupts
in the same function in c file. So we need the data to know the details.
OK, let's move on.
The following exceptions are pretty much the same.
We push index 1 on the stack in the second exception,
the third one, the fourth one and so on.
In the vector8, we don’t push the error code since the CPU pushes the error code on the stack when the exception generates.
So we only push the index number 8 in this case and jump to trap.
The vector9 is reserved, we move to the next exception.
Vector10 through 14
have error codes pushed on the stack by the processor.
Vector15 is reserved
We move on. Vector20 trough 31 are also reserved. In our system,
we don’t handler these exceptions.
If the exception occurs, we just stop the system.
We will see how to do it in a minute.
Then we get to the hardware interrupt handlers.
Remember the timer interrupt is assigned with vector32.
So we push the index 32 and jump to trap.
The spurious interrupt is assigned with vector39.
The process for all the exceptions and interrupts is the same, that is, push error code and index number
then jump to trap.
So let's see the label Trap.
The trap procedure is really simple. We save the state of CPU by pushing the general purpose registers.
Then we increment the character in order to see it changing on the screen.
Then we call handler function in c file. Remember in system v calling convention,
the first argument is passed in register rdi.
What it does is passing the stack pointer to handler.
When we return from it, we reach the end of trap, restore the general purpose registers and return.
Note that
before we return, we need to adjust rsp register to make it point to the correct location.
As you see, we have pushed two value which is 16 bytes on the stack before we jump to trap. So we add rsp,16
to make the rsp point to the original location when the exception or interrupt gets called by the processor.
If the error code is pushed by the processor
like vector 8, we still need to add 8 bytes manually to skip the error code.
So here we simply add 16 in the trap return.
There are other procedures,
such as end of interrupt. As we have seen it before, it send the eoi command to the PIC.
Read isr. Processing spurious interrupt requires reading ISR,
So we add it here.
The last procedure is load idt.
The reason we need this procedure is that we cannot load idt with load idt instruction directly in c.
Instead, we define a procedure load idt
and call this procedure in c file to load idt.
This procedure has one parameter which is the address of idt.
So the parameter is stored in register rdi.
Ok that’s it for trap.asm. Back to the header file.
Since the interrupt handler is defined in the assembly file, we need to declare them in the header file.
As you see, they have no return value and parameters.
Load idt takes one parameter idt pointer. Read isr return a 8-bit value.
Alright,
we look at another file trap.c which sets up the idt.
To use the structures we just defined in the header file, we include trap.h.
We define global variables called idt pointer and vector array which includes 256 items.
Idt pointer is used when we call load idt function. And vector array is actually the new IDT
which has 256 entries in it.
We also use keyword static so that they are visible only in this file, since we don’t reference them in other files
.
OK, let's see the functions.
The initialize idt entry function takes three parameters,
the address of idt entry, the address of handler which we defined in assembly file and the attribute of the idt entry.
It has no return value and it’s a static function which means this function is only visible in this file
.
In the function, we assign the value to each field of the entry.
We copy the lower 16bits of the address to the field low. The selector is 8
which is the kernel code segment selector.
Then copy the attribute and the mid, high parts of the address.
This part is shift right the address 16bits
and this shifts right the address 32bits.
OK, let's continue.
In function initialize idt we initialize idt entry by passing the address of idt entry,
the handler we want to set in the entry and the attribute 8e, the meaning of which is discussed in the last section.
These idt entries are set the same way.
one thing left to do is load the idt pointer.
Copy the limit of the idt which is the length of idt minus 1 and the address of idt. The we load the idt
by calling the function load idt
and we are done.
The last one is handler where all the interrupts are processed here.
The handler has no return value and it only takes one parameter, we call it the trap frame.
The parameter is actually the stack pointer as we have seen it in the assembly file.
Now we define this structure in the header file.
When the handler is called, the state of the stack is like this, the top of the stack is pointing to
the value of r15,
as you see, the general purpose registers,
the index number and some of the error code are pushed manually in the assembly file, the rip rsp rflag value, etc
are pushed by the processor. So we define the trap frame according to it,
each data here is 8 bytes.
Ok at this point, we can access the data on the stack by referencing the item in this structure.
In the handler, we check the index which we pushed on the stack to see which interrupt or exception actually occurs
.
Switch statement is used to do that.
Here we only deal with two interrupts that is the timer and spurious interrupt.
If it is the timer interrupt, the index number we pushed on the stack is 32. We simply send
eoi and return.
If it is the vector 39, we will read isr to see if it is a real interrupt.
We check bit 7 of the return value. If it is set, then it is real interrupt,
we send eoi and return.
If it’s 0, then it is a spurious interrupt,
we simply return.
As for other exceptions and interrupts, we will stop the system because there should be some errors in our kernel
.
The last thing to do before we finish the trap file is add the declaration of init idt entry in the header file,
because we will call the function in main function.
Alright,
the trap file is done.
In the main.c file, we call the init idt function to initialize the idt. Since it is defined in the trap file,
we need to add the header file which includes the declaration of the function.
In the last video, we have changed the kernel file. So we open kernel.asm.
As you see, the interrupt is not enabled. We should enable interrupt, otherwise we will not receive timer interrupt.
So we enable interrupt after the main function returns.
In this system, handlers are supposed to be working after we get to ring3. For the moment
we are still in kernel mode and we want to test timer handler now.
Remember we didn't set up ss register after we enter 64-bit mode.
So we need to load ss segment selector, otherwise the invalid ss selector could be loaded when the handler returns.
In ring0, we can simply load a null descriptor to ss register.
OK, let's edit the build script.
We added two files, trap assembly file and trap c file.
The ouput file of the assembly code is called trapa.o and the output file of c file
is called trap.o.
In the ld command, we added trap.o and trapa.o.
With all the files prepared,
let’s build the project.
OK, we open the bochs to test it out.
You can see character is changing on screen.