Interrupts are like subroutines that you call through ‘interrupt
numbers’ rather than memory addresses. There are interrupts pre-defined
in the BIOS - in the previous post we saw 10h 0eh
prints a
character to the screen - or they can be set up by the programmer.
Interrupts are special because they halt the processor, save the current
state (meaning what? the registers?) to the stack, execute the
interrupt, then restore the pre-interrupt state.
The code for these interrupts are stored in RAM. The locations of the
code are stored in the interrupt vector table
(IVT)1
1 All of
this only applies to Real Mode. In Protected Mode, an “Interrupt
Descriptor Table” is used instead. More on this later ,
which starts right at the beginning of RAM at address
0x00
. There are 256 entries in numerical order,
0x00
to 0xFF
, and each contains a 4 bytes: a 2
byte OFFSET and a 2 byte SEGMENT. This means you can calculate the
location in the IVT of any interrupt code with code * 0x04
.
Interrupt 0x13
is at
0x46
.2
2 Layout
of the IVT:
The processor can throw exceptions with interrupts. For example, if you try to divide by zero in an Intel processor, it will call interrupt 0.3 3 wiki.osdev.org/exceptions is a great resource for learning more
In the following code, we replace interrupt 0 with our own
subroutine, by replacing the entries in the IVT to point to the
subroutine in memory. Running our bootloader in this state will cause
the screen to show AHello World!A
- the first
A
comes from our manual call to the int 0
, and
the second comes from our attempt to divide by zero, which causes the
processor to run interrupt 0.
start:
; snip
mov word[ss:0x00], handle_zero ; Set offset to handle_word address
mov word[ss:0x02], 0x07C0 ; set segment to 0x07c0
int 0 ; call interupt 0
mov si, message
call print
mov ax, 0x00
div ax ; try to divide by 0
jmp $
handle_zero:
mov ah, 0eh
mov al, 'A'
mov bx, 0x00
int 0x10
iret
; snip
Next we will see how we can read data from a hard disk. Note that
we’re not talking about accessing files. Files and the file
system are implemented in the Kernel. Or to be more specific, the disk
is ‘formatted’ with a particular file system data structure (FAT, EXT4
etc.), and the kernel has drivers which are able to interpret that data
structure as files. As far as we are concerned, the disk consists of
blocks of data called
sectors4
4 We
already encountered sectors when talking about how the bootloader is
loaded. .
A sector consists of 512 contiguous bytes. These sectors are read and
written in sector blocks, not by accessing individual
bytes.5
5 Since
you can only read in sectors, to calculate a specific place on the disk
using LBA you need to calculate the sector and the offset. This is
simply a matter of getting the quotient and the modulus. So to get to
byte 58376 you calculate the LBA sector by 58376/512=114
,
and the offset as 58376%512=8
The old way of addressing disk sectors is the Cylinder Head
Sector (CHS) system. This is from when disks were spinning magnetic
plates arranged in cylinders. You need to specify the cylinder, head,
sector and track you want to read from. This was pretty complicated, and
it is no longer really used. The modern way is called Logical Block
Address (LBA). In LBA you just specify the sector number you want
to get. LBA 0 is the first sector on the disk,
etc.5
5 Since
you can only read in sectors, to calculate a specific place on the disk
using LBA you need to calculate the sector and the offset. This is
simply a matter of getting the quotient and the modulus. So to get to
byte 58376 you calculate the LBA sector by 58376/512=114
,
and the offset as 58376%512=8
First create a text file and put in it whatever you want. This is going to be the thing that gets read from the disk. Create a Makefile6 6 Make is a language unto itself, intended to simplify the compilation of project with multiple files. .
all:
nasm -f bin ./boot.asm -o ./boot.bin
dd if=./message.txt >> ./boot.bin
dd if=/dev/zero bs=512 count=1 >> ./boot.bin
The first line is same assemble command we’ve already been using. The
second line puts the content of message.txt onto the end of our
binary, and the third pads the binary out with null characters until
it’s 512 bytes, and therefore a valid sector. You can type
make
at the command line to compile the project. You can
see the content of the binary with
hexdump -C ./boot.bin > hex.txt
, and opening the text
file.
We’ll be using interrupt 13h/02h
: “Disk - Read Sectors
into Memory”. Looking at the expected register values that Ralph Brown
provides7
7
AH
= 02h, AL
= number of sectors to read,
CH
= cylinder number, CL
= sector number,
DH
= head number, ES:BX
-> data buffer.
Return: CF
set on error
we can get to the follow code:
mov ah, 02h
mov al, 1
mov ch, 0
mov dh, 0
mov cl, 2
mov bx, buffer
int 0x13
jc error ; if carry flag is set, meaning load failed
mov si, buffer
call print
; snip to end of file
error_message: db 'Failed to load sector', 0
times 510-($ - $$) db 0 ; Pad to 510th byte
dw 0xAA55 ; dw=define word. Puts bootloader signal
buffer:
Here we set up the registers as they need to be to read our message
from the 2nd sector (cl
) of the disk into the
buffer
label in
memory8
8 Note
that the data is put into ES:BX
, the Extra Segment. We have
set this to 0x7c0
, which is the right place. .
Then we call the interrupt 0x13
. jc
handles
the error condition.