How to create your own OS #5

Rasalingam Ragul
8 min readAug 20, 2021

Hi friends welcome back this is the fifth part of the series if you miss earlier parts go throw this link : List: How to create your own OS (medium.com)

This week we are going to develop our operating system in to next 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 interrupt?

When a Process is executed by the CPU and when a user Request for another Process then this will create disturbance for the Running Process. This is also called as the Interrupt.

Interrupts can be generated by User, Some Error Conditions and also by Software’s and the hardware’s. But CPU will handle all the Interrupts very carefully because when Interrupts are generated then the CPU must handle all the Interrupts Very carefully means the CPU will also Provides Response to the Various Interrupts those are generated. So that When an interrupt has Occurred then the CPU will handle by using the Fetch, decode and Execute Operations.

Types of Interrupts

Generally there are three types o Interrupts those are Occurred For Example

1) Internal Interrupt
2) Software Interrupt.
3) External Interrupt.

The External Interrupt occurs when any Input and Output Device request for any Operation and the CPU will Execute that instructions first For Example When a Program is executed and when we move the Mouse on the Screen then the CPU will handle this External interrupt first and after that he will resume with his Operation.

The Internal Interrupts are those which are occurred due to Some Problem in the Execution For Example When a user performing any Operation which contains any Error and which contains any type of Error. So that Internal Interrupts are those which are occurred by the Some Operations or by Some Instructions and the Operations those are not Possible but a user is trying for that Operation. And The Software Interrupts are those which are made some call to the System for Example while we are Processing Some Instructions and when we wants to Execute one more Application Programs.

Interrupts Handlers

Interrupts are handled via the Interrupt Descriptor Table (IDT). The IDT describes a handler for each interrupt. The interrupts are numbered (0–255) and the handler for interrupt i is defined at the ith position in the table. There are three different kinds of handlers for interrupts:

  • Task handler
  • Interrupt handler
  • Trap handler

The task handlers use functionality specific to the Intel version of x86, so they won’t be covered here (see the Intel manual [33], 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 book, we will use trap handlers and disable interrupts manually when we need to.

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 |

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

The offset is a pointer to code (preferably an assembly code label). For example, to create an entry for a handler whose code starts at 0xDEADBEEF and that runs in privilege level 0 (therefore using the same code segment selector as the kernel) the following two bytes would be used:

0xDEAD8E00
0x0008BEEF

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

idt[0] = 0xDEAD8E00
idt[1] = 0x0008BEEF

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

Once the interrupt handler is done, it uses the iret instruction to return. The instruction iret expects the stack to be the same as at the time of the interrupt (see the figure above). Therefore, any values pushed onto the stack by the interrupt handler must be popped. Before returning, iret restores eflags by popping the value from the stack and 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 use 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. Writing all the logic of the interrupt handler in assembly code will be tiresome. Creating a handler in assembly code that saves the registers, calls a C function, restores the registers and finally executes iret is a good idea!

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

Since the CPU does not push the interrupt number on the stack it is a little tricky to write a generic interrupt handler. This section will use macros to show how it can be done. Writing one version for each interrupt is tedious — it is better to use the macro functionality of NASM [34]. 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:

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.

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:

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. The reasons for configuring the PIC are:

  • Remap the interrupts. The PIC uses interrupts 0–15 for hardware interrupts by default, which conflicts with the CPU interrupts. Therefore the PIC interrupts must be remapped to another 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.

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

The hardware interrupts are shown in the table below:

PIC 1HardwarePIC 2Hardware0Timer8Real Time Clock1Keyboard9General I/O2PIC 210General I/O3COM 211General I/O4COM 112General I/O5LPT 213Coprocessor6Floppy disk14IDE Bus7LPT 115IDE Bus

A great tutorial for configuring the PIC can be found at the SigOPS website [35]. We won’t repeat that information here.

Every interrupt 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:

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:

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.

Now open your terminal and run ‘make run’ then you type ‘c’ in the terminal now you type anything in your keyboard after closing the bochs run ‘cat com1.out’ command now you can see your typing.

this the end of this article if you need more knowledge about this refer the below references,

Reference:

The little book about OS development (littleosbook.github.io)

My GitHub repository : rragul/windOS (github.com)

--

--