Writing your own Operating System: Interrupts and Input

Hasini Samarathunga
CodeX
Published in
7 min readAug 20, 2021
Banner vector created by katemangostar — freepik.com

In one of my previous articles, I explained how to display text on the console and how to write data to the serial port. If you seem confused you can go back to that article to catch up.

So since we have taught our OS to produce output it would be quite nice if it also knows how to read input. So let’s start with the most common source of input; the keyboard. So how does the keyboard work? You just press a button and, voilà, it’s printed on the screen! Is it that simple? This is what we’ll be discussing in this article!

What is an interrupt?

An interrupt occurs when a hardware device, such as a keyboard, a serial port, or a timer, sends an input to the CPU. It’s their way of saying “Excuse me, there has been a change in state, you might need to check this out”.

Interrupts can also be sent by the CPU due to program faults, like when a program divides a number by zero. There are also interruptions caused by the int assembly code instruction. We call them software interrupts and they are mostly used for system calls.

People vector created by pch. vector — freepik.com

Interrupts Handlers

But how do we interpret these interrupts? Well, these Interrupts are handled through the Interrupt Descriptor Table (IDT). There’s a total of 256 different interrupts, each numbered from 0–255. The IDT describes a Handler for each of these interrupts. Now there are 3 kinds of handlers:

  • Task handler — specific to the Intel version of x86
  • Interrupt handler — is triggered by a hardware device
  • Trap handler — is triggered by a user program

In this article, we’ll be using trap handlers.

Creating an Entry in the IDT

Just as we have done countless times in earlier articles, we’ll be using another packed structure to define each entry in the IDT.

Don’t forget to make another packed structure for the IDT itself, just like we did for the GDT.

Handling an Interrupt

But what really happens when an interrupt occurs? First, the CPU will push some information about the interrupt onto the stack. And then it will look up the appropriate interrupt handler in the IDT and jump to it.

The stack at that time will look something like this,

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

The reason for the question mark at the end of the error code? is that not all interrupts create an error code. It’s like asking if there’s an error or not. The error code can be used by the interrupt handler to get more information on what has happened.

Here the interrupt number is not pushed onto the stack because we can know which interrupt is happening just by checking which handler is executing.

Next, once the interrupt handler is done, the stack uses the iret instruction to return.

The iret, like airport customs, will check if the returning stack was the same stack that was there before the interrupt happened. So any values pushed onto the stack by the interrupt handler must be popped. The stack should be the same as before.

Here the C handler should look like this below code.

Next, the tricky part is creating a Generic Interrupt Handler.

The Generic Interrupt Handler

Why is making a Generic Interrupt Handler that can handle every interrupt difficult? It’s simply because the CPU doesn’t push the interrupt number into the stack. To get around this we use the macro functionality of NASM. And since some interrupts don’t produce an error code, the value 0 will be used as the “error code” for them.

So we need an common_interrupt_handler that 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.

So your code should look like this,

Loading the IDT

Next, loading the IDT is done with the lidt assembly code instructions which take the address of the first element in the table.

Programmable Interrupt Controller (PIC)

We can’t just start using the hardware interrupts. For that, we need to configure the Programmable Interrupt Controller (PIC). It’s the PIC that makes it possible to map signals from the hardware to interrupts.

But why do we need to configure it? Can’t the PIC just get the job done?

There are several reasons for configuring the PIC,

  • Remap the interrupts. By default, the PIC uses interrupts 0–15 for hardware interrupts, which are conflicting with the CPU interrupts. Therefore the PIC interrupts must be remapped to a different interval.
  • Select which interrupts to receive. You probably don’t want to receive interrupts from all devices since you don’t have code that handles these interrupts anyway.
  • Set up the correct mode for the PIC.

Here two PICs are chained together to accommodate 16 hardware interrupts.

Every PIC interrupt must be acknowledged, which means that a message must be sent to the PIC confirming that the interrupt has been handled. If this is not done, the PIC will stop generating interrupts. Acknowledging a PIC interrupt is done by sending the byte 0x20 to the PIC that raised the interrupt.

So since the keyboard interrupt is raised by the PIC, you must call pic_acknowledge at the end of the keyboard interrupt handler.

Reading Input from the Keyboard

The next thing we need to fix is the keyboard input itself. The keyboard does not generate ASCII characters, it generates scan codes. A scan code defines a button, both when it is pressed and when it is released. The scan code for the just pressed button can be obtained from the data I/O port on the keyboard, which has the address 0x60.

The final part is to write a function that translates a scan code to the corresponding ASCII character. This code may vary from keyboard to keyboard.

Output the Input

Now just call the serial_write() function and pass the character read from the keyboard to write to the serial port. When you are typing, you won’t see anything on screen (because we are not writing on the Bochs console but to the serial port).

If you have done everything correctly you’ll see this on your com1.out .

But wouldn’t it be even better if whatever we type in the keyboard be displayed on the Bochs console as we are writing it?

For that, we can’t just simply use the fb_write() function because we are only writing a single character at a time and need to move the cursor accordingly.

So, I added unsigned int BUFFER_COUNTto keep track of the number of characters written on the console. And added a fb_clear() function to erase a character when backspace is pressed. You can simply ‘erase’ a character by writing over it.

fb_write_cell( i * 2, ' ', BLACK, BLACK);

--

--