Develop Your Own x86 Operating System(OS) #6
Interrupt handling and reading inputs from keyboard.
Your operating system is currently capable of many functions. As your OS has been developed to produce an output on the console, with this article let’s see how we can advance our OS to obtain inputs from the keyboard. The operating system must be able to handle interrupts in order to read information from the keyboard. Hence, we will also discuss about interrupt handling for a systematic development of the OS.
If you haven’t read the fourth article of the series on accessing memory through segments (segmentation) in the OS, you can read it from here as its content is important to catch up what’s happening in this article.
Interrupts
An interrupt is a condition that needs immediate attention causing the microprocessor to temporarily work on a different task, and then later return to its previous task. Interrupts can be internal or external.
In x86 processor, there are 3 types of interrupts: Hardware Interrupts, Software Interrupts and Exceptions.
Hardware interrupts are triggered by hardware devices. They occur when a hardware device, such as the keyboard, the serial port or the timer, signals the CPU that the state of the device has changed. Then processor stops what it is doing, and executes the code that handles the hardware interrupt. Hardware interrupts are typically asynchronous — their occurrence is unrelated to the instructions being executed at the time they are raised. Ex:- when you type on your keyboard, the keyboard triggers a hardware interrupt and handles keyboard input (typically reading the key you pressed into a buffer in memory).
Software interrupts are used to transfer control to a function in the operating system kernel. Software interrupts are triggered by int assembly code instruction, and they are often used for system calls. Ex:- The instruction “int 14h” triggers interrupt 0x14.
Exceptions are generated by CPU itself in response to program errors, for example when a program references memory it doesn’t have access to, or when a program divides a number by zero. These interrupts are called exceptions. The processor will detect this problem, and transfer control to a handler to service the exception. This handler may re-execute the offending code after changing some value, or if this cannot be done, the program causing the exception to be terminated.
An interrupt condition alerts the processor and serves as a request for the processor to interrupt the currently executing instruction when permitted. If the request is accepted, the processor responds by suspending its current activities, saving its state, and executing a function called an interrupt handler to deal with the interrupt.
Interrupt Handler
Interrupt Handling is the process of identifying the accurate interrupt that has occurred and executing the relevant response for identified interrupt. The job of the interrupt handler is to servee the interrupt and stop it from interrupting. There are three different kinds of handlers for interrupts:
- Task handler
- Interrupt handler
- Trap handler
The task handlers use functionality specific to the Intel version of x86. The only difference between an interrupt handler and a trap handler is that the interrupt handler disables interrupts, which means no interrupts are welcomed at the same time handling an interrupt. In our process, we will use trap handlers and disable interrupts manually as required.
In x86 architecture interrupts are handled by the Interrupt Descriptor Table (IDT).
Interrupt Descriptor Table (IDT)
Interrupt descriptor table (IDT) is an x86 system table that holds descriptors for interrupt handlers (The IDT describes an interrupt handler for each interrupt). The IDT is used by the processor to determine the correct response to interrupts and exceptions. IDT have 256 entries where the interrupts are numbered from 0 to 255. The handler for interrupt i is defined at the ith position in the table.
Interrupt Descriptor Table is similar to the Global Descriptor Table we discussed in the previous article in structure. Before you implementing the IDT, always make sure you have a working GDT.
Creating an Entry in the IDT
The IDT entries are called gates. It can contain Interrupt Gates, Task Gates and Trap Gates. An entry in the IDT for an interrupt handler consists of 64 bits. The highest 32 bits are shown in the figure below:
The lowest 32 bits are presented in the following figure:
The offset is a pointer to code that is to be executed, probably an assembly code label. For example, to create an entry for a handler whose code starts at 0xDEADBEEF and that runs in privilege level 0 the two bytes 0xDEAD8E00 and 0x0008BEEF would be used.
If the IDT is represented as an unsigned integer idt, then to register the above example as an handler for interrupt 0 (divide-by-zero), the following code would be used:
idt [0] = 0xDEAD8E00
idt [1] = 0x0008BEEF
Instead of using bytes or unsigned integers it’s better to use packed structures we discussed in the previous article to make the code more readable.
In order to create an entry to IDT first make a file called interrupts.h in your working directory to save following C function declarations:
Let’s skip function definitions for now as we have to study more to come up with them.
Loading the IDT
The IDT is loaded with the lidt assembly code instruction which takes the address of the first element in the table. We wrap this instruction and use it from C as follows:
Create idt.s in your working directory and save the above code in it as shown in the image below:
Handling an Interrupt
When an interrupt occurs the CPU will push some information about the interrupt onto the stack, then look up the appropriate interrupt handler in the IDT and jump to it. The stack at the time of the interrupt will look like the following:
[esp + 12] eflags
[esp + 8] cs
[esp + 4] eip
[esp] error code?
The reason for the question mark behind error code is that not all interrupts create an error code. The specific CPU interrupts that put an error code on the stack are 8, 10, 11, 12, 13, 14 and 17. The error code can be used by the interrupt handler to get more information on the interrupt that has taken place. Moreover, Interrupt Number is not pushed onto the stack. So, interrupt that has occurred can be identified only by knowing the code that is executing. Ex:- if the handler registered for interrupt 14 is executing, then interrupt 14 has occurred.
Once the interrupt handler has done its task, it uses the iret instruction to return. The instruction iret expects the stack to be the same as at the time of the interrupt. Hence, any values pushed onto the stack by the interrupt handler should be popped from the stack. Before returning, iret restores eflags by popping the value from the stack and then finally jumps to cs: eip as specified by the values on the stack.
All registers that the interrupt handlers use must be preserved by pushing them onto the stack therefore, the interrupt handler must be written in assembly code. The reason for this is that the code which is being interrupted doesn’t know about the interrupt and will therefore expect that its registers stay the same. But, writing all the logic of the interrupt handler in assembly code will be wearisome. Therefore we can Create a handler in assembly code that saves the registers, calls a C function, restores the registers and finally executes iret.
The C handler should get the state of the registers, the state of the stack and the number of the interrupt as arguments. The following definition can be used:
Now update your interrupts.h file with the above code as follows:
Function definition of interrupt handler can also be done later with further studies.
Creating a Generic Interrupt Handler
It’s not really easy to write a generic interrupt handler as the CPU does not push the interrupt number on the stack . So, let’s see the way we can use macros to do it. Writing one version for each interrupt is not a good idea so it is better to use the macro functionality of NASM. Since only some of interrupts produce an error code, the value 0 will be stated as the “error code” for interrupts without an error code. The following code shows how this can be done:
Create interrupt_handler.s file in your working directory and save the above code in it:
The common interrupt handlers performs the following:
- Push the registers on the stack.
- Call the C function interrupt_handler.
- Pop the registers from the stack.
- Add 8 to esp.
- Execute iret to return to the interrupted code.
Since the macros declare global labels, the addresses of the interrupt handlers can be accessed from C or assembly code when creating the IDT.
Programmable Interrupt Controller (PIC)
Programmable Interrupt Controller is used to manage hardware interrupts from different hardware devices and send them to the appropriate system interrupt. Therefore in order to start using hardware interrupts first the Programmable Interrupt Controller (PIC) must be configured. The PIC makes it possible to map signals from the hardware to interrupts.
The reasons for configuring the PIC are:
- Remapping the interrupts. The PIC uses interrupts 0–15 for hardware interrupts by default, which conflicts with the CPU interrupts. Therefore the PIC interrupts must be remapped to another interval.
- Selecting which interrupts to receive.
- Setting up the correct mode for the PIC.
In the beginning there was only one PIC (PIC 1) and eight interrupts. With the addition of more hardware, 8 interrupts were not sufficient. So, as solution another PIC (PIC 2) was chained on the first PIC.
Every interrupt from the PIC has to be acknowledged by sending a message to the PIC confirming that the interrupt has been handled. If this isn’t done the PIC won’t generate any more interrupts.
The process of acknowledging a PIC interrupt is done by sending the byte 0x20 to the PIC that raised the interrupt. Implementing a pic_acknowledge function can thus be done as follows:
For the fully functioning of PIC let’s create a file named pic.h and save the following code in it:
After that function definitions including pic_acknowledge should be saved in a file called pic.c as follows:
Reading Input from the Keyboard
Now our OS can handle interrupts, so we are ready to obtain inputs from the keyboard. In this section let’s see the way we can do it.
The keyboard does not generate ASCII characters, instead it generates scan codes. A scan code represents a button and more specifically both presses and releases of the particular button. Keyboard’s data I/O port which has address 0x60 can be used to read the scan code representing the just pressed button. How this can be done is shown in the following example:
The next step is to write a function that translates a scan code to the corresponding ASCII character. For this create a keyboard.h file first and save the following macro definitions and function declarations:
So let’s create keyboard.c file with definitions of above functions:
Since the keyboard interrupt is raised by the PIC, you must call pic_acknowledge at the end of the keyboard interrupt handler. And also, the keyboard will not send you any more interrupts until you read the scan code from the keyboard.
So now, we have learned all necessary details to create interrupts.c file that we skipped earlier. It contains definitions for functions stated in interrupts.h. The following code can be used for this:
The following two functions should be included in interrupts.h file which are wrappers around NASM.
void load_idt(unsigned int idt_address);
void interrupt_handler_33();
as follows:
Our input from the keyboard will be outputted in com1.out file we introduced in a former article. So, let’s make a small change in the serial_write function we declared in the serial_port.c file and serial_port.h file ( Created in the fourth article of the series). Update serial_port.c file with the following function definition and serial_port.h file with it’s declaration as follows:
As usual it’s time to update our kmain.c file as follows:
Then update OBJECT variable of Makefile as follows:
Using the “make run” command boot your OS, and input what you want on the console( In the marked place) using your keyboard.
Now open your co1.out file with a text editor and you will see your input saved in the file.
Now, you have developed your own operating system to handle interrupts and read input from the keyboard. In the next article let’s discuss about how to easily execute a small program in kernel mode. Read it form here.
References
x86 Assembly/Programmable Interrupt Controller
Hope you understand steps in handling interrupts and reading inputs from the keyboard. Let’s meet with the next article of Develop Your Own x86 Operating System(OS) series. Thank you so much for reading!!!!!!!!!!
Isuruni Rathnayaka