Operating System Development from Scratch - Interrupts - Part 5
Hello everybody, it’s me, Lovelace! In this post, you will learn what interrupts are, the Interrupt Vector Table and you'll also implement your own interrupts in Real Mode.
Error Fixing
I forgot something past entry and it was to fix an error that might couldn't let us boot on an actual device (even though that entry is all about it), let's fix that error first before we continue to learn about interrupts.
BPB Table taken from osdev.
The problem is, that in some computers our bootloader will boot perfectly fine, but in some others, there might be some errors as the BIOS tampers your data when booting from a USB stick, which is probably the way we all will test our operating system. The reason this happens is because of the BIOS Parameter Block some BIOSes expect it, while some others won't corrupt your data and everything will work fine. The BIOSes that require it will fill this information and that's how our data will be broken.
That data is tampered because when we boot from a USB stick, it is doing something called USB emulation, the BIOS is treating our USB stick as a hard drive and allowing us to talk to it as such. You don't need to know much about the BPB right now, we just need to know that some BIOSes assume it's there and start writing data overwriting your code.
We can get around this problem by implementing the BIOS Parameter Block, we don't need to have real values it can be all zeros, we will create a fake BPB to get around this problem.
The first thing we have to do is to add up the size of the BIOS Parameter Block, except the first three bytes (because they are a short jump and a nop and some BIOSes will look for this as well, we will actually write these first three bytes, and we will rest the other ones with zeros).
To do this, go to the start of your code (before jmp 0x7c0:start
) and create another label, for example:
_start:
And what we will do, is basically add:
jmp short start
nop
We are doing the short jump and the nop of the first three bytes (Note: nop
means "No Operation").
Now we have to create another label under start
so we can do a jump to that new label so it sets the code segment at 0x7c0
, just modify the beginning of your start
label so it looks like this:
start:
jmp 0x7c0:step2
step2:
;; Rest of the code here
Note: You also have to delete the old jmp 0x7c0:start
line.
Now, we can put our fake BPB in the middle of _start
and the middle of start
. After the No Operating instruction, let's add 33 zeros (as that's the added num of bytes the BPB occupies):
times 33 db 0
Now you should be able to assemble and boot your bootloader without the risk of code being overwritten by the BPB.
Interrupt Vector Table
What are interrupts?
Interrupts are something like subroutines, but you don't need to know their memory address to invoke them, you call them through the use of interrupt numbers such as 1
, 2
and 3
, rather than memory addresses, interrupts can be set up by the programmer, for example, you could set the interrupt 0x32
to point it somewhere in your code, therefore, when someone does int 0x32
it will invoke the defined interrupt.
What happens when an interrupt is invoked?
First, the processor gets interrupted, the old state is pushed to the stack, saving it, the old state includes things such as the return address and after that, our interrupt is executed.
What is the Interrupt Vector Table?
The interrupt vector table is a table describing where these interrupts are in memory, we have 256 interrupt handlers and each of those entries in the table is 4 bytes (the first two bytes is the offset in memory, the next two bytes is the segment in memory). Interrupts are also in numerical order in the table.
The interrupt vector table starts to absolute address 0 in RAM, it's the first byte in memory. The first four bytes describe interrupt 0, the next four bytes describe interrupt one, the next four ones interrupt two and so on until interrupt 256. The following is a fictitious IVT:
Offset | Segment |
---|---|
0x00 | 0x7c0 |
0x00 | 0x7f00 |
0x00 | 0x7c10 |
0x00 | 0x7c80 |
As you could imagine, each of these interrupts is called by their number (1
, 2
, 3
and 4
, in order) and when the offset and the segment address are added, it will return the absolute address of the routine that executes this interrupt.
Implementing the IVT and creating an interrupt
Go to your boot.asm
file and what we will do, is to create an interrupt. To do so, first create a label:
handle_int0:
iret
And what this interrupt will do, is to display a character to the screen so add the following in the handle_int0
label:
handle_int0:
mov ah, 0eh
mov al, 'A'
mov bx, 0x00
int 0x10
iret
Note: We add the iret
keyword at the end of all the interrupts we defined, it represents the end of an interrupt.
What we are going to do now, is to change the Interrupt Vector table, so when we call the interrupt 0
it will execute our handle_int0
interrupt.
With what I mentioned before, we know that the interrupt 0 stars at address 0x00
in RAM, so the first two bytes of RAM are the offset for the interrupt, and the next two bytes are the segment for the interrupt 0, so we will only need to do:
mov word [ss:0x00], handle_int0
mov word [ss:0x02], 0x7c0
int 0
After we initialise the registers and we enable the interrupts in the step2
routine.
Note: We use the stack segment (when doing mov word [ss:hex_num]
), because if we don't tell the assembler to use the stack segment, it will use the data segment which currently points at 0x7c0
which would be a problem, that's why we add ss:
which means an address in the stack segment. We could also change the data segment and then set it back with the value it should have, but this way is just easier.
Now if we assemble and run our program, by running:
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -hda boot.bin
We would get an output like the following:
Interrupt 0
The Interrupt 0 is an exception when one number is divided by zero, so what we can do now, is to divide one number by zero and our interrupt will be called when we do that. You can divide by zero doing:
mov ax, 0x00
div ax
Note: Don't forget to delete int 0
.
That piece of code will set the value of the ax
register to zero and then divide it by itself. Our interrupt would be called again anyway.
You can get more information about the available exceptions here.
You can see this entry's changes here.