Let’s Build an Own Operating System (PrimitiveOS)

Sachin Tharaka
11 min readAug 20, 2021

Part 5- Interrupts and Inputs

Welcome Back!

This is my journey through making a new Operating System named PrimitiveOS.This is the fifth article of the article series and after reading this you can get a proper idea about interrupts in an OS.

Before entering this Please read previous parts if you haven’t already done so. In the last article, I had written about how to do segmentation and booting an OS.

https://tharakasachin98.medium.com/lets-build-an-own-operating-system-primitiveos-6a71ae5e6213

I believe you must remember that in our 3rd week we display outputs to our console.

https://tharakasachin98.medium.com/lets-build-an-own-operating-system-primitiveos-ae4a60ea5c34

We did not utilize the keyboard to enter data; instead, we used a given string. That’s because if we wish to manage keyboard inputs, our operating system must also be able to handle keyboard interrupts. But our operating system wasn’t equipped to handle interruptions at the time. So, this week, we’ll progress our operating system to a new level that can deal with interruptions.

Now we can jump into today's topic which is about What is interrupt and why do we need to handle it?

Interrupts and Input

It would be good if the OS could also receive input now that it can make output. When a hardware device, such as a keyboard, serial port, or timer, tells the CPU that its status has changed, an interrupt occurs. Interrupts can also be sent by the CPU due to program errors, such as when a program refers to memory it doesn’t have access to or divides a value by zero. Finally, software interrupts, which are interrupts caused by the int assembly code instruction and are frequently used for system calls.

an Interrupt?

An interrupt is a signal from a device 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.

There are generally three classes of interrupts on most platforms

  • Exceptions: These are created by the CPU internally.
  • Interrupt Request (IRQ) or Hardware Interrupt: This type of interrupt is generated externally by the chipset.
  • Software Interrupt: This is an interrupt is a signal sent by software.

But we Assume that hardware interrupts and software interrupts are the main two interrupts.

Hardware Interrupts.

Hardware devices send interrupts when the state of that device has been changing. For example when the user presses the button ‘A’ the state of the keyboard changes or when the user presses the ‘right click’ button the state of the mouse changes

Software Interrupts.

A software interrupts 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 and figure out what has happened(Which interrupt has occurred). When the operating system identifies what has happened then it needs to follow up a special routine according to the interrupt that has been occurred.

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

Interrupt Handlers

There is a slew of hardware and software issues. As a result, before the OS can handle such interrupts, it must first determine which interrupt has occurred. Then it must run a procedure that is appropriate for that particular interrupt. Interrupt Handling is the term for this.

When the interrupt occurs the CPU gets 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 instruction.

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

  • Task handler
  • Interrupt handler
  • Trap handler

Interrupt Handler Vs 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 an 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.

The next 32bits can be shown as follows.

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

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

Let’s consider an example to clarify this more. Imagine that create an entry for an interrupt and its handler whose code starts at 0xDEADBEEF and that runs in privilege level 0. We can use the 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 a 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 identified with a unique number. This is known as IRQ number or interrupts request number. For more information about IRQ numbers refer to the following table here.

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

Handling an Interrupt

When an interrupt occurs, the system must proceed through three basic phases.

Step 1 -- Save the current state of the process

Step 2 --Handle the interrupt

Step 3 -- Restore the CPU process and execute it

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. The following is an example of the information that is placed into the stack:

[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 codes can be used by the interrupt handler to get more information on what has happened. Also, there is a point to highlight 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 completes its execution, it uses the iret instruction to return the output. The instruction iret expects the stack to be the same as at the time 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 used 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 in C language. So let’s create 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 create a background for executing the interrupt. Now let’s jump into the part of coding the interrupt handler which actually executes the interrupt.

I created a file named interrupts.h

Generic Interrupt Handler

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:

Make assembly file interrupt_handlers.s

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.It is easiest to wrap this instruction and use it from C:

Make the assembly file idt.s with the following code.

Programmable Interrupt Controller (PIC)

A programmable interrupt controller (PIC) helps the 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 was 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 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 raises 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 the interrupts which were handling is completed. Then only the PIC can move on for another interrupt.

On most systems, there are two PICs, each with eight separate inputs and one output signal that informs the CPU that an IRQ has occurred. Because the slave PIC’s output signal is coupled to the master PIC’s third input (input #2), when the slave PIC wants to inform the CPU of an interrupt, it actually informs the master PIC, who then informs the CPU. This is referred to as “cascade.” IRQ 2 cannot occur because the master PIC’s third input is configured for this rather than as a standard IRQ.

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

Every interrupts from the PIC has to be acknowledged — that is, 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.

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:

Create the pic.c file

For accessing interrupts we want interrupts.c file

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 PIC raised the keyboard interrupt, 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.

Special Notes

When the boch is started you cannot see a place to give your keyboard inputs but you have to enter something by the keyboard.

then you have to exit from the emulator and you can open com1.out file. there will be your expecting output

When you accessing the file paths you must consider file paths.

Example. Imagine that you have files abc.c inside the folder ABC and outside of ABC folder abc.h file exists. Then you have to give the path for calling abc.c file in abc.h as ‘ABC/abc.c’

If you have the expected output, Congratulations. You have done this part successfully.

Thank you for reading. We will meet soon.#staysafe #stayconnected

Reference: https://littleosbook.github.io/ https://intermezzos.github.io/
http://os.phil-opp.com/

For any issue with making files, you can follow my repository(Branch interupts_and_inputs))

https://github.com/Sachin-Tharaka/PrimitiveOS

--

--

Sachin Tharaka

Software Engineering, University of Kelaniya, Sri Lanka