Writing Your Own Toy OS: Krishnakumar R. Raghu and Chitkala
Writing Your Own Toy OS: Krishnakumar R. Raghu and Chitkala
Writing Your Own Toy OS: Krishnakumar R. Raghu and Chitkala
By
Krishnakumar R.
Assembled by
Zhao Jiong
[email protected]
2002-10-6
Writing Your Own Toy OS
This article is a hands-on tutorial for building a small boot sector. The first
section provides the theory behind what happens at the time the computer is
switched on. It also explains our plan. The second section tells all the things
you should have on hand before proceeding further, and the third section deals
with the programs. Our little startup program won't actually boot Linux, but
it will display something on the screen.
1. Background
First write a small program in 8086 assembly (don't be frightened; I will teach
you how to write it), and copy it to the boot sector of the floppy. To copy,
we will code a C program. Boot the computer with that floppy, and then enjoy.
ld86
gcc
A free floppy
A floppy will be used to store our operating system. This also is our boot
device.
as86 and ld86 will be in most of the standard distributions. If not, you can
always get them from the site http://www.cix.co.uk/~mayday/. Both of them are
included in single package, bin86. Good documentation is available at
www.linux.org/docs/ldp/howto/Assembly-HOWTO/as86.html.
3. 1, 2, 3, Start!
entry start
3
Writing Your Own Toy OS
start:
mov ax,#0xb800
mov es,ax
seg es
mov [0],#0x41
seg es
mov [1],#0x1f
loop1: jmp loop1
This is an assembly language that as86 will understand. The first statement
specifies the entry point where the control should enter the program. We are
stating that control should initially go to label start. The 2nd line depicts
the location of the label start (don't forget to put ":" after the start). The
first statement that will be executed in this program is the statement just after
start.
0xb800 is the address of the video memory. The # is for representing an immediate
value. After the execution of
mov ax,#0xb800
register ax will contain the value 0xb800, that is, the address of the video
memory. Now we move this value to the es register. es stands for the extra segment
register. Remember that 8086 has a segmented architecture. It has segments like
code segments, data segments, extra segments, etc.--hence the segment registers
cs, ds, es. Actually, we have made the video memory our extra segment, so anything
written to extra segment would go to video memory.
To display any character on the screen, you need to write two bytes to the video
memory. The first is the ascii value you are going to display. The second is
the attribute of the character. Attribute has to do with which colour should
be used as the foreground, which for the background, should the char blink and
so on. seg es is actually a prefix that tells which instruction is to be executed
next with reference to es segment. So, we move value 0x41, which is the ascii
value of character A, into the first byte of the video memory. Next we need to
move the attribute of the character to the next byte. Here we enter 0x1f, which
is the value for representing a white character on a blue background. So if we
execute this program, we get a white A on a blue background. Finally, there is
the loop. We need to stop the execution after the display of the character, or
we have a loop that loops forever. Save the file as boot.s.
The idea of video memory may not be very clear, so let me explain further. Suppose
we assume the screen consists of 80 columns and 25 rows. So for each line we
need 160 bytes, one for each character and one for each character's attribute.
If we need to write some character to column 3 then we need to skip bytes 0 and
1 as they is for the 1st column; 2 and 3 as they are for the 2nd column; and
4
Writing Your Own Toy OS
then write our ascii value to the 4th byte and its attribute to the 5th location
in the video memory.
int main()
{
char boot_buf[512];
int floppy_desc, file_desc;
boot_buf[510] = 0x55;
boot_buf[511] = 0xaa;
First, we open the file boot in read-only mode, and copy the file descripter
of the opened file to variable file_desc. Read from the file 510 characters or
until the file ends. Here the code is small, so the latter case occurs. Be decent;
close the file.
The last four lines of code open the floppy disk device (which mostly would be
/dev/fd0). It brings the head to the beginning of the file using lseek, then
writes the 512 bytes from the buffer to floppy.
The man pages of read, write, open and lseek (refer to man 2) would give you
enough information on what the other parameters of those functions are and how
5
Writing Your Own Toy OS
to use them. There are two lines in between, which may be slightly mysterious.
The lines:
boot_buf[510] = 0x55;
boot_buf[511] = 0xaa;
This information is for BIOS. If BIOS is to recognize a device as a bootable
device, then the device should have the values 0x55 and 0xaa at the 510th and
511th location. Now we are done. The program reads the file boot to a buffer
named boot_buf. It makes the required changes to 510th and 511th bytes and then
writes boot_buf to floppy disk. If we execute the code, the first 512 bytes of
the floppy disk will contain our boot code. Save the file as write.c.
cc write.c -o write
First, we assemble the boot.s to form an object file boot.o. Then we link this
file to get the final file boot. The -d for ld86 is for removing all headers
and producing pure binary. Reading man pages for as86 and ld86 will clear any
doubts. We then compile the C program to form an executable named write.
./write
Reset the machine. Enter the BIOS setup and make floppy the first boot device.
Put the floppy in the drive and watch the computer boot from your boot floppy.
Then you will see an 'A' (with white foreground color on a blue background).
That means that the system has booted from the boot floppy we have made and then
executed the boot sector program we wrote. It is now in the infinite loop we
had written at the end of our boot sector. We must now reboot the computer and
remove the our boot floppy to boot into Linux.
From here, we'll want to insert more code into our boot sector program, to make
it do more complex things (like using BIOS interrupts, protected-mode switching,
etc). The later parts (PART II, PART III etc. ) of this article will guide you
on further improvements. Till then GOOD BYE !
6
Writing Your Own Toy OS
Krishnakumar R.
7
Writing Your Own Toy OS
The next thing that any one should know after learning to make a boot sector
and before switching to protected mode is, how to use the BIOS interrupts. BIOS
interrupts are the low level routines provided by the BIOS to make the work of
the Operating System creator easy. This part of the article would deal with BIOS
interrupts.
1. Theory
For example for printing something on the screen we call the C function like
this :
8
Writing Your Own Toy OS
display(noofchar, attr);
int 0x10
One important thing is that the same interrupt can be used for a variety of
purposes. The purpose for which a particular interrupt is used depends upon the
function number selected. The choice of the function is made depending on the
value present in the ah register. For example interrupt 13h can be used for
displaying a string as well as for getting the cursor position. If we move value
3 to register ah then the function number 3 is selected which is the function
used for getting the cursor position. For displaying the string we move 13h to
register ah which corresponds to displaying a string on screen.
9
Writing Your Own Toy OS
Using interrupt 13h, the boot sector loads the second sector of the floppy into
memory location 0x5000 (segment address 0x500). Given below is the source code
used for this purpose. Save the code to file bsect.s.
LOC1=0x500
entry start
start:
mov ax,#LOC1
mov es,ax
mov bx,#0
mov dl,#0
mov dh,#0
mov ch,#0
mov cl,#2
mov al,#1
mov ah,#2
int 0x13
jmpi 0,#LOC1
The first line is similar to a macro. The next two statements might be familiar
to you by now. Then we load the value 0x500 into the es register. This is the
address location to which the code in the second sector of the floppy (the first
sector is the boot sector) is moved to. Now we specify the offset within the
segment as zero.
Next we load drive number into dl register, head number into dh register, track
number into ch register, sector number into cl register and the number of sectors
to be transferred to registeral. So we are going to load the sector 2, of track
number 0, drive number 0 to segment 0x500. All this corresponds to 1.44Mb floppy.
Now we call interrupt 13h and finally jump to 0th offset in the segment 0x500.
10
Writing Your Own Toy OS
entry start
start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#26
mov bx,#0x0007
mov bp,#mymsg
mov ax,#0x1301
int 0x10
mymsg:
.byte 13,10
.ascii "Handling BIOS interrupts"
This code will be loaded to segment 0x500 and executed. The code here uses
interrupt 10h to get the current cursor position and then to print a message.
The first three lines of code (starting from the 3rd line) are used to get the
current cursor position. Here function number 3 of interrupt 13h is selected.
Then we clear the value in bh register. We move the number of characters in the
string to register ch. To bx we move the page number and the attribute that is
to be set while displaying. Here we are planning to display white characters
on black background. Then address of the message to be be printed in moved to
register bp. The message consists of two bytes having values 13 and 10 which
correspond to an enter which is the Carriage Return (CR) and the Line Feed (LF)
together. Then there is a 24 character string. Then we select the function which
corresponds to printing the string and then moving the cursor. Then comes the
call to interrupt. At the end comes the usual loop.
5. The C program
The source code of the C program is given below. Save it into file write.c.
int main()
{
11
Writing Your Own Toy OS
char boot_buf[512];
int floppy_desc, file_desc;
boot_buf[510] = 0x55;
boot_buf[511] = 0xaa;
lseek(floppy_desc, 0, SEEK_SET);
write(floppy_desc, boot_buf, 512);
close(floppy_desc);
}
In PART I of this article I had given the description about making the boot floppy.
Here there are slight differences. We first copy the file bsect, the executable
code produced from bsect.s to the boot sector. Then we copy the sect2 the
executable corresponding to sect2.s the second sector of the floppy. Also the
changes to be made to make the floppy bootable have also been performed.
6. Downloads
You can download the sources from
1. bsect.s
LOC1=0x500
entry start
start:
mov ax,#LOC1
mov es,ax
mov bx,#0 ;segment offset
12
Writing Your Own Toy OS
jmpi 0,#LOC1
2. sect2.s
entry start
start:
mov ah,#0x03 ; read cursor position.
xor bh,bh
int 0x10
mymsg:
.byte 13,10
.ascii "Handling BIOS interrupts"
3. write.c
int main()
{
char boot_buf[512];
int floppy_desc, file_desc;
13
Writing Your Own Toy OS
boot_buf[510] = 0x55;
boot_buf[511] = 0xaa;
lseek(floppy_desc, 0, SEEK_SET);
write(floppy_desc, boot_buf, 512);
close(floppy_desc);
}
4. Makefile
bsect : bsect.o
ld86 -d bsect.o -o bsect
sect2 : sect2.o
ld86 -d sect2.o -o sect2
bsect.o : bsect.s
as86 bsect.s -o bsect.o
sect2.o : sect2.s
as86 sect2.s -o sect2.o
write : write.c
cc write.c -o write
14
Writing Your Own Toy OS
clean :
rm bsect.o sect2.o bsect sect2 write
7. What Next?
After booting with the floppy you can see the string being displayed. Thus we
will have used the BIOS interrupts. In the next part of this series I hope to
write about how we can switch the processor to protected mode. Till then, bye !
Krishnakumar R.
15
Writing Your Own Toy OS
In Parts I and II of this series, we examined the process of using tools available
with Linux to build a simple boot sector and access the system BIOS. Our toy
OS will be closely modelled after a `historic' Linux kernel - so we have to switch
to protected mode real soon! This part shows you how it can be done.
16
Writing Your Own Toy OS
17
Writing Your Own Toy OS
We have yet another table called the interrupt descriptor table or the IDT. The
IDT contains the interrupt descriptors. These are used to tell the processor
where to find the interrupt handlers. It contains one entry per interrupt, just
like in Real Mode, but the format of these entries is totally different. We are
not using the IDT in our code to switch to the protected mode so further details
are not given.
Switching to protected mode essentially implies that we set the PE bit. But there
are a few other things that we must do. The program must initialise the system
segments and control registers. Immediately after setting the PE bit to 1 we
have to execute a jump instruction to flush the execution pipeline of any
instructions that may have been fetched in the real mode. This jump is typically
to the next instruction. The steps to switch to protected mode then reduces to
the following :
18
Writing Your Own Toy OS
3. What we need
• a blank floppy
• NASM assembler
print_mesg :
mov ah,0x13 ; Fn 13h of int 10h writes a whole string on screen
mov al,0x00 ; bit 0 determines cursor pos,0->point to start after
mov bx,0x0007 ; bh -> screen page ie 0,bl = 07 ie white on black
mov cx,0x20 ; Length of string here 32
mov dx,0x0000 ; dh->start cursor row,dl->start cursor column
int 0x10 ; call bios interrupt 10h
ret ; Return to calling routine
get_key :
mov ah,0x00
int 0x16 ; Get_key Fn 00h of 16h,read next character
ret
clrscr :
mov ax,0x0600 ; Fn 06 of int 10h,scroll window up,if al = 0 clrscr
mov cx,0x0000 ; Clear window from 0,0
mov dx,0x174f ; to 23,79
mov bh,0 ; fill with colour 0
19
Writing Your Own Toy OS
begin_boot :
call clrscr ; Clear the screen first
mov bp,bootmesg ; Set the string ptr to message location
call print_mesg ; Print the message
call get_key ; Wait till a key is pressed
bits 16
call clrscr ; Clear the screen
mov ax,0xb800 ; Load gs to point to video memory
mov gs,ax ; We intend to display a brown A in real mode
mov word [gs:0],0x641 ; display
call get_key ; Get_key again,ie display till key is pressed
mov bp,pm_mesg ; Set string pointer
call print_mesg ; Call print_mesg subroutine
call get_key ; Wait till key is pressed
call clrscr ; Clear the screen
cli ; Clear or disable interrupts
lgdt[gdtr] ; Load GDT
mov eax,cr0 ; The lsb of cr0 is the protected mode bit
or al,0x01 ; Set protected mode bit
mov cr0,eax ; Mov modified word to the control register
jmp codesel:go_pm
bits 32
go_pm :
mov ax,datasel
mov ds,ax ; Initialise ds & es to data segment
mov es,ax
mov ax,videosel ; Initialise gs to video memory
mov gs,ax
mov word [gs:0],0x741 ; Display white A in protected mode
spin : jmp spin ; Loop
bits 16
gdtr :
dw gdt_end-gdt-1 ; Length of the gdt
dd gdt ; physical address of gdt
gdt
nullsel equ $-gdt ; $->current location,so nullsel = 0h
gdt0 ; Null descriptor,as per convention gdt0 is 0
dd 0 ; Each gdt entry is 8 bytes, so at 08h it is CS
dd 0 ; In all the segment descriptor is 64 bits
20
Writing Your Own Toy OS
Type in the code to a file by name abc.asm. Assemble it by typing the command
nasm abc.asm. This will produce a file called abc. Then insert the floppy and
type the following command dd if=abc of=/dev/fd0. This command will write the
file abc to the first sector of the floppy. Then reboot the system. You should
see the following sequence of messages.
• Our os booting........................
• A (Brown colour)
• Switching to protected mode....
• A (White colour)
21
Writing Your Own Toy OS
As mentioned in the previous article (Part 1) the BIOS selects the boot device
and places the first sector into the address 0x7c00. We thus start writung our
code at 0x7c00.This is what is implied by the org directive.
FUNCTIONS USED
print_mesg: This routine uses the subfunction 13h of BIOS interrupt 10h to write
a string to the screen.The attributes are specified by placing appropriate
values in various registers. Interrupt 10h is used for various string
manipulations.We store the subfn number 13h in ah which specifies that we wish
to print a string. Bit 0 of the al register determines the next cursor position;if
it is 0 we return to the beginning of the next line after the function call,
if it is 1 the cursor is placed immediately following the last character printed.
The video memory is split into several pages called video display pages.Only
one page can be displayed at a time(For further details on video memory refer
Part 1).The contents of bh indicates the page number,bl specifies the colour
of the character to be printed. cx holds the length of the string to be
printed.Register dx specifies the cursor position. Once all the attributes have
been initialised we call BIOS interrupt 10h.
get_key: We use BIOS interrupt 16h whose sub function 00h is used to get the
next character from the screen. Register ah holds the subfn number.
clrscr: This function uses yet another subfn of int 10h i.e 06h to clear the
screen before printing a string.To indicate this we initialise al to 0.Registers
cx and dx specify the window size to be cleared;in this case it is the entire
screen. Register bh indicates the colour with which the screen has to be
filled;in this case it is black.
In Real-Mode :
22
Writing Your Own Toy OS
We don't need any interrupts bothering us,while in protected mode do we ?So lets
disable them(interrupts that is).That is what cli does. We will enable them
later.So lets start by setting up the GDT.We initialise 4 descriptors in our
attempt to switch to protected mode. These descriptors initialise our code
segment(code_gdt), data and stack segments (data_gdt) and the video segment in
order to access the video memory. A dummy descriptor is also initialised although
it's never used except if you want to triple fault of course. This is a null
descriptor. Let us probe into some of the segment descriptor fields.
• The first word holds the limit of the segment, which for simplicity is
assigned the maximum of FFFF(4G). For the video segment we set a predefined
value of 3999 (80 cols * 25 rows * 2bytes - 1).
• The base address of the code and data segments is set to 0x0000. For the
video segment it is 0xb8000 (Video Memory base address).
The GDT base address has to be loaded into GDTR system register. The gdtr segment
is loaded with the size of the GDT in the first word and the base address in
the next dword. The lgdt instruction then loads the gdt segment into the GDTR
register.Now we are ready to actually switch to pmode. We start by setting the
least significant bit of CR0 to 1( ie the PE bit).We are not yet in full protected
mode!
Section 10.3 of the INTEL 80386 PROGRAMMER'S REFERENCE MANUAL 1986 states :
Immediately after setting the PE flag,the initialization code must flush the
processor's instruction prefetch queue by executing a JMP instruction.The 80386
fetches and decodes instructions and addresses before they are used; however,
after a change into protected mode, the prefetched instruction information
(which pertains to real-address mode) is no longer valid. A JMP forces the
processor to discard the invalid information.
We are in protected mode now. Want to check it out? Let's get our A printed in
white. For this we initialise the data and extra segments with the data segment
selector (datasel). Initialise gs with the video segment selector (videosel).
23
Writing Your Own Toy OS
To display a white 'A' move a word containing the ascii value and attribute to
location [gs:0000] ie b8000 : 0000. The spin loop preserves the text on the screen
until the system is rebooted.
24