Build your own operating system #interrupts_and_inputs

Sadisha Nimsara
8 min readAug 20, 2021

In previous article, we discussed about how you can integrate segmentation to your operating system. Hope you could do it successfully. In this article, we are going to learn how we can handle interrupts and get inputs with our operating system.

Let’s go.

What is an interrupt?

Interrupt is a signal emitted by hardware or software when a process or an event needs immediate attention. It alerts the processor to a high-priority process requiring interruption of the current working process. In I/O devices one of the bus control lines is dedicated for this purpose and is called the Interrupt Service Routine (ISR).

When a device raises an interrupt at let’s say process i, the processor first completes the execution of instruction i. Then it loads the Program Counter (PC) with the address of the first instruction of the ISR. Before loading the Program Counter with the address, the address of the interrupted instruction is moved to a temporary location. Therefore, after handling the interrupt the processor can continue with process i+1.

While the processor is handling the interrupts, it must inform the device that its request has been recognized so that it stops sending the interrupt request signal. Also, saving the registers so that the interrupted process can be restored in the future, increases the delay between the time an interrupt is received and the start of the execution of the ISR. This is called Interrupt Latency.

Interrupt Handlers

There are a number of hardware and software interruptions. Before manipulating those barriers, the operating system must identify what obstacles have been encountered. It is then necessary to implement the relevant routine for that specific interruption. This is known as the interrupt handling.

This can be done by creating a table called Interrupt Descriptor Table (IDT). This table describes routine that OS need to follow for each interrupt. The interrupts are numbered (0–255) and the handler(or the routine) for interrupt i is defined at the i th position in the table.

All interrupts cannot be handled in the same way. There are three main types of interrupt handling.

  • Task handler
  • Interrupt handler
  • Trap handler

Here we focus on mainly interrupt handler and trap handler because task handlers use functionality specific to the Intel version of x86. The difference between interrupt handler and trap handler is easy to understand. That is interrupt handler disables other interrupts while handling one interrupt. But trap handlers do not disable other interrupts like that. So in here, we have to disable those other interrupts manually when necessary.

Creating an Entry in the IDT

Since there are many different interrupts, each interrupt must be registered with the OS in order to identify them individually. 64 bits are required to register one record in IDT.

The first 32bits can be shown as follows.

The lowest 32 bits are presented in the following figure:

A description for each name can be found in this table:

Here the offset means that, 32bit memory address pointer to the memory which contain the code that need to execute.

Let’s consider an example to clarify this more. Imagine that create an entry for a interrupt and it’s handler whose code starts at 0xDEADBEEF and that runs in privilege level 0. We can use following two bytes to represent that address and other details.

0xDEAD8E00
0x0008BEEF

If the IDT is represented as an unsigned integer idt[512] 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

Now I believe you can figure out how to register an interrupt for the operating system. In this method, each interrupt can be identified by a unique number. This is known as the IRQ number or Interrupt Request Number. For more information on IRQ numbers, refer to the following table.

Handling an Interrupt

When an interrupt occurs, the CPU will push some interrupt information onto the stack, then check up and hop to the proper interrupt handler in the IDT.

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 what has happened. Also, note that the interrupt number is not pushed onto the stack. We can only determine what interrupt has occurred by knowing what code is executing — if the handler registered for interrupt 17 is executing, then interrupt 17 has occurred.

When the interrupt handler is complete it’s execution, it uses the iret instruction to return the output. The instruction iret expects the stack to be the same as at the time of the interrupt occurred. Therefore, any values pushed onto the stack by the interrupt handler must be removed from the stack. That is why iret restores eflags by removing the value from the stack. Then finally jumps to cs:eip as specified by the values on the stack.

The interrupt handler has to be written in assembly code, since all registers that the interrupt handlers use must be preserved by pushing them onto the stack. This is because the code that was interrupted doesn’t know about the interrupt and will therefore expect that its registers stay the same. Writing all the logic of the interrupt handler in assembly code will be tiresome. Creating a handler in assembly code that saves the registers, calls a C function, restores the registers and finally executes iret.

The interrupt handler written in C language should get the state of the registers(struct cpu_state and struct stack_state) the state of the stack and the number of the interrupt as arguments. The following definitions can for example be used:

Creating a Generic Interrupt Handler

Since the CPU does not push the interrupt number on the stack it is a little tricky to write a generic interrupt handler. This section will use macros to show how it can be done. Writing one version for each interrupt is tedious — it is better to use the macro functionality of NASM [34]. And since not all interrupts produce an error code the value 0 will be added as the “error code” for interrupts without an error code. The following code shows an example of how this can be done:

The common_interrupt_handler does the following:

After creating IDT these interrupt handler codes can be access by C language or Assembly.

Loading the IDT

The IDT is loaded with the lidt assembly code instruction which takes the address of the first element in the table. It is easiest to wrap this instruction and use it from C:

Programmable Interrupt Controller (PIC)

A programmable interrupt controller (PIC) helps CPU to handle interrupt requests (IRQ) coming from multiple different sources (like external I/O devices) which may occur simultaneously. It helps prioritize IRQs so that the CPU switches execution to the most appropriate interrupt handler (ISR) after the PIC assesses the IRQ’s relative priorities.

To start using hardware interrupts you must first configure the Programmable Interrupt Controller (PIC). The PIC makes it possible to map signals from the hardware to interrupts.

In the beginning there was only one PIC (PIC 1) and eight interrupts. As more hardware were added, 8 interrupts were too few. The solution chosen was to chain on another PIC (PIC 2) on the first PIC (see interrupt 2 on PIC 1).

The hardware interrupts are shown in the table below:

PICs allow you to map inputs and outputs in a configurable way. This is important because every PIC interrupt must be acknowledged. This means sending a PIC message confirming that the interrupt has been handled. If this is not done, the PIC will no longer generate interrupts.

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:

Reading Input from the Keyboard

The keyboard does not generate ASCII characters, it generates scan codes. A scan code represents a button — both presses and releases. The scan code representing the just pressed button can be read from the keyboard’s data I/O port which has address 0x60. How this can be done is shown in the following example:

#include "io.h"

#define KBD_DATA_PORT 0x60

/** read_scan_code:
* Reads a scan code from the keyboard
*
* @return The scan code (NOT an ASCII character!)
*/
unsigned char read_scan_code(void)
{
return inb(KBD_DATA_PORT);
}

The next step is to write a function that translates a scan code to the corresponding ASCII character. This can be done as follows by using keyboard.c file.

Since the keyboard interrupt is raised by the PIC, it is a must to call pic_acknowledge at the end of the keyboard interrupt handler. Also as I mentioned before, the keyboard will not send any more interrupts until that it reads the scan code from the keyboard.

If you managed to do all the steps correctly, you can view your results by executing cat com1.out command. This com1.out file will contain whatever you type in your keyboard.

You can download a completed code that I have created for handling interrupts and inputs for the OS from: here

Hope you have successfully implemented handling interrupts and inputs to your OS and hope to catch you in the next article.

Thank you!

--

--