YmeOS — Let’s Build our own OS !!! 😉

Thushara Samaraweera
5 min readAug 20, 2021

#5- Interrupts and input

Hello everyone! Welcome to the fifth installment of my OS implementing series, in which I demonstrate how to build an operating system from scratch.

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

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.

Interrupts Handlers

The Interrupt Descriptor Table is used to manage interrupts (IDT). For each interrupt, the IDT specifies a handler. The interrupts are numbered (0–255), and the handler for interrupt I am defined at the table’s ith place. Interrupt handlers are divided into three categories:

  • Task handler
  • Interrupt handler
  • Trap handler

The task handlers use functionality specific to the Intel version of x86, See the Intel manual, chapter 6, for more info. The only difference between an interrupt handler and a trap handler is that the interrupt handler disables interrupts, which means you cannot get an interrupt while at the same time handling an interrupt. In this article series, we will use trap handlers and disable interrupts manually when we need to.

All registers used by interrupt handlers must be preserved by pushing them onto the stack, hence the interrupt handler must be written in assembly code. Because the interrupted code is unaware of the interrupt, it will anticipate its registers to remain unchanged. It will be tedious to write all of the logic for the interrupt handler in assembly code.

Creating an Entry in the IDT

An entry in the IDT for an interrupt handler consists of 64 bits. The highest 32 bits are shown in the figure below:

Bit: | 31 16 | 15 | 14 13 | 12 | 11 | 10 9 8 | 7 6 5 | 4 3 2 1 0 |
Content: | offset high | P | DPL | 0 | D | 1 1 0 | 0 0 0 | reserved |

The lowest 32 bits are presented in the following figure:

Bit:     | 31              16 | 15              0 |
Content: | segment selector | offset low |

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 hander 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 C handler should get the state of the registers, 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);

Creating a Generic Interrupt Handler

It’s a little hard to create a general interrupt handler since the CPU doesn’t push the interrupt number into the stack. This section will demonstrate how to accomplish it with macros. It’s easier to use NASM’s macro functionality instead of writing one version for each interrupt. Because not all interruptions generate an error code, the number 0 will be used as the “error code” for interrupts that do not generate one. An example of how this may be done is shown in the code below:

Interrupt handlers.s is the name of the file you should make.

Loading the IDT

It is easiest to wrap this instruction and use it from C:

global  load_idt    ; load_idt - Loads the interrupt descriptor table (IDT).
; stack: [esp + 4] the address of the first entry in the IDT
; [esp ] the return address
load_idt:
mov eax, [esp+4] ; load the address of the IDT into register eax
lidt eax ; load the IDT
ret ; return to the calling function

Programmable Interrupt Controller (PIC)

A programmable interrupt controller (PIC) assists the CPU in handling interrupt requests (IRQ) from multiple sources (such as external I/O devices) that may occur concurrently. It aids in the prioritization of IRQs so that the CPU can switch execution to the most appropriate interrupt handler (ISR) after the PIC evaluates the relative priorities of the IRQs.

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

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.

Update your kmain.c file like this.

Remember, since the keyboard interrupt is raised by the PIC, you must call pic_acknowledge at the end of the keyboard interrupt handler. Also, the keyboard will not send you any more interrupts until you read 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 on your keyboard.

References:

The Little OS Book: https://littleosbook.github.io/book.pdf

Thank you very much for reading!

Hope to see you again with next chapter. Till then, STAY SAFE!!!

Happy reading …

-Thushara Samaraweera-

--

--