Build your own operating system

Nirmal Kapilarathne
8 min readAug 20, 2021

Part 05

Interrupts and Input

Hello everyone!

This is the fifth article of the “Build your own Operating System“ article series. First of all, I like to suggest, please refer to previous articles, before reading this. It will help you to better understanding of this article.

Now that the OS can produce output it would be nice if it also could get some input. Refer below article to know about how to display text on the console as well as writing data to the serial port.

What is an Interrupt?

When a process or event requires immediate attention, hardware or software emits an interrupt signal. It informs the processor to a high-priority operation that requires the present working process to be interrupted. One of the bus control lines in I/O devices is allocated to this purpose and is known as 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 handling interrupts, the processor must inform the device that its request has been recognized so that the interrupt request signal is no longer sent. Furthermore, storing the registers so that the interrupted process can be restored in the future adds to the time between receiving an interrupt and the start of the ISR execution. Interrupt Latency is the term for this.

Interrupt Handlers

A variety of hardware and software issues have arisen. The operating system must first determine what impediments have been faced before modifying those barriers. The appropriate process for that specific interruption must then be implemented. Interrupt handling is the term for this.

This can be accomplished by constructing an Interrupt Descriptor Table (IDT). This table outlines the procedure that the OS must follow for each interrupt. The interrupts are numbered (0–255), and the handler (or function) for interrupt I is defined in the table at the i th location.

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

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] = 0xDEAD8E00idt[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.

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.

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);

Creating a Generic Interrupt Handler

It’s a little tough to design a general interrupt handler because the CPU doesn’t push the interrupt number into the stack. This section will demonstrate how to do it with macros. It’s easier to use NASM’s macro functionality instead of writing one version for each interrupt. 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:

%macro no_error_code_interrupt_handler %1
global interrupt_handler_%1
interrupt_handler_%1:
push dword 0 ; push 0 as error code
push dword %1 ; push the interrupt number
jmp common_interrupt_handler ; jump to the common handler
%endmacro

%macro error_code_interrupt_handler %1
global interrupt_handler_%1
interrupt_handler_%1:
push dword %1 ; push the interrupt number
jmp common_interrupt_handler ; jump to the common handler
%endmacro

common_interrupt_handler: ; the common parts of the generic interrupt handler
; save the registers
push eax
push ebx
.
.
.
push ebp

; call the C function
call interrupt_handler

; restore the registers
pop ebp
.
.
.
pop ebx
pop eax

; restore the esp
add esp, 8

; return to the code that got interrupted
iret

no_error_code_interrupt_handler 0 ; create handler for interrupt 0
no_error_code_interrupt_handler 1 ; create handler for interrupt 1
.
.
.
error_code_handler 7 ; create handler for interrupt 7
.
.
.

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:

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)

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.

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.

First, add the header file corresponding to the c file.

#include "io.h"

#define PIC1_PORT_A 0x20
#define PIC2_PORT_A 0xA0

/* The PIC interrupts have been remapped */
#define PIC1_START_INTERRUPT 0x20
#define PIC2_START_INTERRUPT 0x28
#define PIC2_END_INTERRUPT PIC2_START_INTERRUPT + 7

#define PIC_ACK 0x20

/** pic_acknowledge:
* Acknowledges an interrupt from either PIC 1 or PIC 2.
*
* @param num The number of the interrupt
*/
void pic_acknowledge(unsigned integer interrupt)
{
if (interrupt < PIC1_START_INTERRUPT || interrupt > PIC2_END_INTERRUPT) {
return;
}

if (interrupt < PIC2_START_INTERRUPT) {
outb(PIC1_PORT_A, PIC_ACK);
} else {
outb(PIC2_PORT_A, PIC_ACK);
}
}

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.

#include "io.h"
#define KEYBOARD_DATA_PORT 0x60
unsigned char keyboard_read_scan_code(void)
{
return inb(KEYBOARD_DATA_PORT);
}
unsigned char keyboard_scan_code_to_ascii
(
unsigned char scan_code)
{
unsigned char ascii[256] =
{
0x0, 0x0, '1', '2', '3', '4', '5', '6', // 0 - 7
'7', '8', '9', '0', '-', '=', 0x0, 0x0, // 8 - 15 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 16 - 23
'o', 'p', '[', ']', '\n', 0x0, 'a', 's', // 24 - 31 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 32 - 39
'\'', '`', 0x0, '\\', 'z', 'x', 'c', 'v', // 40 - 47
'b', 'n', 'm', ',', '.', '/', 0x0, '*', // 48 - 55
0x0, ' ', 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 56 - 63
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, '7', // 64 - 71
'8', '9', '-', '4', '5', '6', '+', '1', // 72 - 79
'2', '3', '0', '.' // 80 - 83
};
return ascii[scan_code];
}

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.

Thank you very much for reading!

--

--