The XamOS — Let’s Build an OS!!!

Nipuni Perera
7 min readAug 20, 2021

--

The 5th step of OS development — Interrupts and Input

Hola Readers!

If you’re new to OS development and my article series you better come from this first article.Otherwise it would be hard to understand what’s going on.🤔

It would be great if the OS could also receive input now that it can create output. (To receive information from the keyboard, the operating system must be able to handle interrupts.) When a hardware device, such as a keyboard, serial port, or timer, informs the CPU that its status has changed, an interrupt occurs. Interrupts can also be sent by the CPU due to software mistakes, 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 interruptions triggered by the int assembly code instruction and are frequently utilized for system calls, exist.

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

Because the task handlers are exclusive to the Intel version of x86, they will not be addressed here. The only difference between an interrupt handler and a trap handler is that an interrupt handler suppresses interruptions, so you can’t get an interrupt while handling one. In this series of articles, we’ll learn how to use trap handlers and manually deactivate interrupts as necessary.

Creating an Entry in the IDT

An interrupt handler’s entry in the IDT is 64 bits long. In the figure below, the top 32 bits are shown:

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 shown in the figure below:

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

The table below has a description for each name:

--------------------------------------------------------------------
Name | Description
--------------------------------------------------------------------
offset high | The 16 highest bits of the 32 bit address in the
| segment.
offset low | The 16 lowest bits of the 32 bits address in the
| segment.
p | If the handler is present in memory or not
| (1 = present, 0 = not present).
DPL | Descriptor Privilige Level, the privilege level the
| handler can be called from (0, 1, 2, 3).
D | Size of gate, (1 = 32 bits, 0 = 16 bits). segment
| selector The offset in the GDT.
r | Reserved.

The offset is a code pointer (preferably an assembly code label). For example, the following two bytes might be used to generate an entry for a handler whose code begins at 0xDEADBEEF and operates under privilege level 0 (therefore utilizing the same code segment selector as the kernel):

0xDEAD8E00

0x0008BEEF

If the IDT is represented as an unsigned integer idt[512], then the following code would be used to register the previous example as an interrupt 0 (divide-by-zero) handler:

idt[0] = 0xDEAD8E00

idt[1] = 0x0008BEEF

To make the code more understandable, we propose utilizing packed structures instead of bytes (or unsigned integers), as described in the second article of this series about “Implement with C.”

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. At the moment of the interrupt, the stack will look like this:

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

Because not all interruptions generate an error code, the error code is preceded by a question mark. 8, 10, 11, 12, 13, 14, and 17 are the particular CPU interrupts that cause an error code to be added to the stack. The interrupt handler can utilize the error code to find out more about what went wrong. It’s also worth noting that the interrupt number isn’t moved to the top of the stack. We can only tell what interrupt has happened by looking at the code that is running — if the handler for interrupt 17 is running, then interrupt 17 has happened.

When the interrupt handler is finished, it returns with the iret command. The stack should be the same as it was at the moment of the interrupt, according to the command iret (see the figure above). As a result, any values that the interrupt handler has placed onto the stack must be popped. Before returning, iret pops the value from the stack to restore eflags, and then jumps to cs:eip as defined by the values on the stack.

Because all registers used by interrupt handlers must be maintained by pushing them onto the stack, the interrupt handler must be written in assembly code. This is because the interrupted code is unaware of the interruption and so expects its registers to remain unchanged. It will be tedious to write all of the interrupt handler functionality in assembly code. It’s a good idea to write an assembly handler that saves the registers, runs a C function, restores the registers, and then executes iret.

The following code says how I created the C handler in interrupt. h:

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.

The common_interrupt_handler does the following:

>Push the registers on the stack.
>Call the C function interrupt_handler.
>Pop the registers from the stack.
>Add 8 to esp (because of the error code and the interrupt number pushed earlier).
>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.

Interrupt.c (accessing interrupt):

Loading the IDT

The lidt assembly code instruction, which takes the address of the table’s first element, is loaded into the IDT. Wrapping this instruction and using it from C is the simplest option.

Programmable Interrupt Controller (PIC)

You must first set up the Programmable Interrupt Controller before you can use hardware interrupts (PIC). Signals from the hardware can be mapped to interrupts using the PIC. The following are the reasons for configuring the PIC:

• Reconfigure the interrupts. By default, the PIC utilizes interrupts 0 through 15 for hardware interrupts, which clashes with the CPU interrupts. As a result, the PIC interrupts need to be remapped to a different time period.

• Choose which interruptions you want to receive. You probably don’t want to accept interrupts from all devices because you don’t have any code to deal with them.

• Select the appropriate PIC mode.

There was just one PIC (PIC 1) and eight interruptions at first. Eight interrupts were insufficient when additional circuitry was introduced. The method adopted was to add a second PIC (PIC 2) to the first PIC (see PIC 1 interrupt 2).

The following table lists the hardware interrupts:

--------------------------------------------------------------------
PIC 1 | Hardware | PIC 2 | Hardware --------------------------------------------------------------------
0 | Timer | 8 | Real Time Clock
1 | Keyboard | 9 | General I/O
2 | PIC 2 | 10 | General I/O
3 | COM 2 | 11 | General I/O
4 | COM 1 | 12 | General I/O
5 | LPT 2 | 13 | Coprocessor
6 | Floppy disk | 14 | IDE Bus
7 | LPT 1 | 15 | IDE Bus

The SigOPS website has a fantastic guide for setting the PIC. That information will not be repeated here. Every PIC interrupt must be acknowledged, which means that a message must be sent to the PIC verifying that the interrupt has been handled. If this is not done, the PIC will stop generating interrupts. Sending the byte 0x20 to the PIC that raised the interrupt acknowledges the interrupt.
As a result, implementing a pic acknowledge function is as follows:

Reading Input from the Keyboard

The keyboard creates scan codes rather than ASCII characters. A scan code describes a button, both when it is pressed and when it is released. The scan code for the recently pushed button may be retrieved from the data I/O port on the keyboard, which contains the address 0x60. The following example demonstrates how this may be done:

All the things implement by following C code Keyboard. c file:

The next step is to create a function that converts a scan code to an ASCII character. Andries Brouwer provides a nice guide if you wish to map the scan codes to ASCII characters like on an American keyboard. You must call pic to acknowledge the conclusion of the keyboard interrupt handler since the keyboard interrupt is raised by the PIC. Also, until you read the scan code from the keyboard, the keyboard will not send you any more interruptions.

Thank you very much for reading!

I’ll hope to get back to you with the chapter five, “integrate_user_modes ” as soon as possible.Till then,

Stay Safe!!! 👋

-Nipuni Perera-

--

--