Develop our own operating system! #part05

Interrupts and Inputs

Waruni Lalendra
9 min readAug 20, 2021

Hello everyone! Welcome back to exciting 5th step of building our own operating system. I believe you have followed me up-to now. If you have missed any step I urge you to cover them up because for this week lesson covering up all the pervious lessons is a must!

I believe you must remember that, in our 3rd week we display outputs to our console. But we did not use the key board for input data, we have done it with codes only. That’s because if we want to handle inputs from the keyboard our operating system should know how to handle interrupts from the key board. But at that moment our operating system wasn’t ready for handling interrupts. So this week we are going to develop our operating system in to another stage which can handle the interrupts. That brings us to our main topic today. What is interrupt and why do we need to handle it?

What is an Interrupt?

An interrupt is a signal from a device that attached to our computer or from a program within the computer that requires the operating system to stop the current process and figure out what to do next. As you can see there are basically two types of interrupts.

Hardware devices send interrupts when the state of that device has been change. For example when user press the button ‘A’ the state of the keyboard changes or when the user press the ‘right click’ button the state of the mouse changes. Those interrupts are known as Hardware Interrupts.

The other type is Software Interrupts. A software interrupt often occurs when an application software terminates or when it requests the operating system for some service. Other than that it happens by an exceptional condition in the processor itself, for example when a program divides a number by zero.

A software interrupt is invoked by software, unlike a hardware interrupt, and is considered one of the ways to communicate with the kernel or to invoke system calls (A system call is a communication mechanism between a process and the operating system. This is a programmatic way that a computer program requests a service from the OS kernel.), especially during error or exception handling.

Now you can understand that when this kind of interruption happens the operating system must stop what it is doing right now and need to figure out what has happened(Which interrupt has occurred). When the operating system identify what has happened then it need to follow-up a special routine according to the interrupt that has been occurred. So how we can do this?

When the keyboard raised a interrupt, the CPU must know that it is a interrupt raised by the keyboard(identify the interrupt) and the CPU need to follow the routine which is needed to handle it.

Interrupt Handlers

There are number of hardware and software interrupts. So before handle those interrupts the OS must identify which interrupt has been occurred. Then it need to execute relevant routine for that specific interrupt. This is known as 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.

When the interrupt is occurred CPU get the number of that interrupt. Using that number the interrupt descriptor table which is in the memory, points to the relevant interrupt handler routine for the emerged interrupt. Then the CPU can execute that instructions.

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

  • Task handler
  • Interrupt handler
  • Trap handler

Here we are focusing 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 and every interrupt must be registered to the OS to identify them separately. To register one entry to the IDT it takes 64bits.

The first 32bits can be shown as follows.

Next 32bits can be shown as follows.

The descriptor table explains that the content of above 64 bit interrupt.

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 (the interrupt for divide-by-zero exception), the following code would be used:

    idt[0] = 0xDEAD8E00
idt[1] = 0x0008BEEF

Now I believe you can understand that how to register an interrupt for the operating system. In this method each and every interrupt can be identify with it’s unique number. This is known as IRQ number or interrupt request number. For your more information about IRQ numbers refer the following table here.

After the CPU finds the entry for the interrupt, it jumps to the code that entry points to. Then that code is run in response to the interrupt is known as a interrupt service routine (ISR) or an interrupt handler as I mentioned before.

Handling an Interrupt

When an interrupt occurs there are three main steps that has to complete by the system.

  1. Save the current state of the process
  2. Handle the interrupt
  3. Restore the CPU process and execute it

So now let’s focus on what is going on in this second step and third step.

So when an interrupt occurs the CPU will push some information about the interrupt onto the stack , then look up the appropriate interrupt hander in the IDT and jump to it. The information that pushed into stack can be presented as follows:

    [esp + 12] eflags
[esp + 8] cs
[esp + 4] eip
[esp] error code?

You can see that there is a question mark behind the error code. That is because we do not consider all interrupts as error codes. The CPU interrupts that put an error code on the stack are 8, 10, 11, 12, 13, 14 and 17. These error code can be used by the interrupt handler to get more information on what has happened. Also there is a point to highlight in here. That is the interrupt number is not pushed onto the stack. We can identify what interrupt has occurred by knowing what code is executing.

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.

But since writing them all in assembly code will be tedious. So let’s do this on C language. So let’s creating a handler in assembly code that saves the registers, calls a C function, restores the registers and finally executes iret code.

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:

    struct cpu_state {
unsigned int eax;
unsigned int ebx;
unsigned int ecx;
.
.
.
unsigned int esp;
} __attribute__((packed));

struct stack_state {
unsigned int error_code;
unsigned int eip;
unsigned int cs;
unsigned int eflags;
} __attribute__((packed));

void interrupt_handler(struct cpu_state cpu, struct stack_state stack, unsigned int interrupt);

These codes creates a background for executing the interrupt. Now let’s jump in to the part of coding the interrupt handler which actually execute the interrupt.

Creating a Generic Interrupt Handler

I have mentioned that before, the CPU doesn’t push the interrupt number on the stack. So we have to write a generic interrupt handler. We will use macros to show how it can be done. A macro is a sequence of instructions, assigned by a name and could be used anywhere in the program. Writing one version for each interrupt is time consuming. So it is better to use the macro functionality of NASM. 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:

This common interrupt handler executes as follows.

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

Loading the IDT

The IDT is loaded to the register with the lidt assembly code instruction which takes the address of the first element in the interrupt descriptor table.

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.

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.
When there are two PICs the main PIC which is directly connected to the CPU known as MASTER PIC and the other one known as SLAVE PIC.

The below table shows the hardware that raise interrupts from 0–15.

PICs allow mapping input to outputs in a configurable way. This is important because every interrupt from the PIC has to be acknowledged. That means, 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.

That process is s done by sending the byte 0x20 to the PIC that raised the interrupt. Implementing a pic_acknowledge function can thus be done as follows:

With this you can inform the PIC that interrupt which was handling is completed. Then only the PIC can move on for another interrupt.

Reading Input from the Keyboard

Rather than producing ASCII characters, the keyboard creates scan code characters. Basically, a scan code is a button that may be pressed or released. Data I/O port 0x60 on the keyboard may be used to read the scan code for the button that was pressed just now. The following example illustrates how this can be accomplished:

    #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.

You can check your result by cat com1.out command. This com1.out file will contain whatever you type in your keyboard! Hope you have a good idea about interrupts now. So, till next week, be motivated and keep learning! :)

Written by,

R.A.W. Lalendra

Undergraduate in Bsc(hons) Software Engineering

University of Kelaniya Sri Lanka.

--

--

Waruni Lalendra

Software Engineering undergraduate at University of Kelaniya Sri Lanka