First thing we need to create a boot sector. Because CPU is loading in 16 bit mode,
to create a boot sector we need to use assembler and a linker from bin86 package.
There are of course alternatives, but the tutorial will be created using examples from this package.
The syntax for this assembler combining common characteristics for Intel and AT&T and might
seem a little strength, however you'll get used to it.
1. Boot sector (boot.s)
I will not write the full source for the program, so that you will understand the basic idea better.
First of all, we should define the general constants:
START_HEAD = 0 - The device head, we will be using.
START_TRACK = 0 - The track, we start reading from.
START_SECTOR = 2 - The sector, we will start reading the OS from
SYSSIZE = 10 - The size in sectors (1 sector = 512 byte)
FLOPPY_ID = 0 -The ID of the drive (0 - is first, 1 is second)
HEADS = 2 - Number of heads
SECTORS = 18 - Number of sectors for a floppy ( 18 for 1.44 Mb)
While booting the following will happen:
The BIOS loader reads first sector of the floppy and will put it in 0000:0x7c00
and will move there the control. We'll get it and, first, we should move ourselves
lower to the address 0000:0x600 move there and continue. All we really need to do
is to boot the kernel ( sector 2-12 on the first track of the floppy with the address
0x100:0000, moving into protected mode and jumping to the first rows of the kernel.
Because of this, we'll need to declare few more constants:
BOOTSEG = 0x7c00 - We will put the booting BIOS sector here
INITSEG = 0x600 - Here we will move it
SYSSEG = 0x100 - Our kernel will go here
DATA_ARB = 0x92 - The definer of data segment for descriptor
CODE_ARB = 0x9A - The definer of code segment for descriptor
First of all, we should move ourselves into more appropriate place:
cli
xor ax,ax
mov ss,ax
mov sp, #BOOTSEG
mov si, sp
mov ds, ax
mov es, ax
sti
cld
move di, #INITSEG
mov cx, #0x100
repnz
movsw
jmpi go, #0 ; jump into new place of the boot sector , mark "go"
Now we have to configure data segments (es,ds)
go:
mov ax, #0xF0
mov ss,ax
mov sp, ax
0xF0:0xF0=0xFF0
mov ax, #0x60 ;we'll set data segments in 0x60
mov ds,ax
mov es,ax
Finally, we can output the message. Because we have the whole BIOS
we can use the prepared function 0x13 of interrupt 0x10. We could
of course cut it and write directly into Video memory, but we only
have 512 bytes, which is not a lot. We'd rather spend it on something else.
mov cx,#18
mov bp,#boot_msg
call write_message
The function write_message looks like this:
write_message:
push bx
push ax
push cx
push dx
push cx
mov ah, #0x03; read the current cursor position, so that we don't output the message anywhere.
xor bh,bh
int 0x10
pop cx
mov bx,#0x0007 ; parameters of outputted symbols: videopage 0, attribute 7 (black and grey)
mov ax,#0x1301 ; outputting prompt and moving cursor
int 0x10
pop dx
pop cx
pop ax
pop bx
ret
and message is like this:
boot_msg:
.byte 13,10
.ascii "Booting MiniOS..."
.byte 0
By this time we will have message "Booting MiniOS..." displayed on the screen.
This is much better than hello world :).
(BOOT.S)
Now, as we have our loader, we move into protected mode and output program written in C.
The kernel is 32 bit. It will be placed separately from the boot sector and built with
gcc and gas. The syntax of assembler gas is compatible with AT&T standard,
so it will be much simpler. But first we need to read kernel. Again, we'll
use a prepared function 0x2 of interrupt 0x13.
recalibrate:
mov ah, #0
mov dl, #FLOPPY_ID
int 0x13 ; drive reinitialisation
jc recalibrate
call read_track ; calling kernel reading function
jnc next_work ; if nothing bad happens, continue working
bad_read ; if reding is not successful, output error
mov bp,#error_read_msg
mov cx, 7
call write_message
inf1: jmp inf1 ; going to endless loop, so that we can be saved by "hand" rebooting only.
The actual reading function is very simple. We should "memorise" parameters and
the read the kernel in one go. The difficulties will begin when the kernel wont
be able to fit in 17 sectors (i.e. 8.5 Kb); But this is in the future and now we
shouldn't have troubles with that!
read_track:
pusha
push es
push ds
mov di, #sysseg
mov es, di ; the data buffer address
xor bx,bx
mov ch, #START_TRACK ; track 0
mov c1, #START_SECTOR ; start from sector 2
mov dl, FLOPPY_ID
mov dh, #START_HEAD
mov ah, #2
mov al, #SYSSIZE ; read 10 sectors
int 0x13
pop ds
pop es
popa
ret
That's it, kernel is read successfully, and we can output one more message:)
next_work:
call kill_motor ; stop floppy drive
mov bp,#load_msg ; output message
mov cx,#4
call write_message
this is the message, you want to output
load_msg:
.ascii "DONE"
.byte 0
And here how you stop floppy:
kill_motor:
push dx
push ax
mov dx,#0x3f2
xor al,al
out dx,al
pop ax
pop dx
ret
No, you should see the following on your monitor "Booting MiniOS...DONE" and the lame
of your floppy drive should be off. Ok, now is the most important bit, jump
into protected mode. First of all, we should turn on the address line A20. It
means that we'll be using 32 bit data addressing.
mov al, #0x01 ; the recording command for 8042
out #0x64, al
mov al, #0xDF
out #0x60, al
Show to everyone, that we are moving into protected mode, so that everyone knows how 31337 we are:).
protected_mode:
mov bp,#load_msg
mov cx, #25
call write_message
the message
load_msg:
.byte 13,10
.ascii "Going to protected mode..."
.byte 0
While the BIOS is still alive, we should save the cursor position and save it in (0000:0x8000).
Kernel will use this data after.
save_cursor:
mov ah,#0x03 ; read cursor position
xor bh, bh
int 0x10
seg cs
mov [0x8000], dx ; save into special hidden place
Now, ATTENTION , restrict interrupts and load descriptors table.
cli
igdt GDT_DESCRIPTOR ; load descriptors table description
The descriptors table consists of 3 describers: zero ( should present always), code segment and data segment.
.align 4
.word 0
GDT_DESCRIPTORS: .word 3*8-1 ; descriptors table size
.long 0x600 + GDT ; location of descriptors table
.align 2
GDT:
.long 0,0 ; e.g. 0 : empty descriptor
.word 0xFFFF, 0 ; number 8: code descriptor
.byte 0, CODE_ARB, 0xC0, 0
.word 0xFFFF, 0 ; number 0x10 : data descriptor
.byte 0, DATA_ARB, 0xCF, 0
Moving into protected mode may implemented in (at minimum) two ways. We
will be using command lmsw, just like
in Linux.
mov ax, #1
lmsw ax ; later real mode : now we are in a protected mode
jmpi 0x1000, 8 ; Long jump onto 32 bit kernel
At the end of the assembly file you could also add:
.org 511
.end_boot: .byte 0
As a result, your file will be exactly 512 byte.
That's it, we are done!!! Now we say good buy to it and move onto kernel.
(START.C)
Unfortunately, the kernel must also start from assembler. However now it wont be so much of it.
We should set the correct values of segments for data. (es, ds, fs, gs) Saving there the
value of respective data descriptor.
cld
cli
movl $(___KERNEL_DS),%eax
movl %ax,%ds
movl %ax,%es
movl %ax,%fs
movl %ax,%gs
No we check, whether address line A20 is turned on correctly with a simple writing methd.
For the experiment to be successful we should zero flags
xorl %eax,%eax
1: incl %eax
mov %eax,0x000000
cmpl %eax,0x100000
je 1b
pushl $0
popfl
Now we call the function, written in C.
call SYMBOL_NAME (start_my_kernel)
And that's it
inf: jmp inf
Now we are almost back to what we started from. Almost, because we now need to create printf
"by hands". Because there are no interrupts, we will be writing directly into video memory.
I will "borrow" the code for this part from Linux (arch/i386/boot/compressed) with minor
changes. For assembling you'll need
The following macros: inb(), outb(), inb_p(), out_p(). Look up in Linux. Now, so that
built in function of glibc don't confuse us, we cancel them
#undef memcpy
set some of our own functions:
static void puts(const char *);
static char vidmem =(char)0xb8000;
/*video memory address*/
static int vidport; /*videoport*/
static int lines,cols; /* number of lines and colors on a screen*/
static int curr_x,curr_y; /*current cursor position*/
Now, we will start program in a normal (high level) language with some more assembly of course:).
The function ov moving cursor into (x,y). The is done via I/O into videoport.
void gotoxy(int x, int y)
{
int pos;
pos=(x+cols*y)*2
outb_p(14,vidport);
outb_p(0xff & (pos>>9),vidport+1);
outb_p(15,vidport);
outb_p(0xff &(pos>>1),vidport+1);
}
Screen scrolling function. Works using direct writing into video memory.
static void puts(const char*s)
{
int x,y;
char c;
x=curr_x;
y=curr_y
while((c=*s++)!=``){
if(c==``){
x=0;
f(++y>=lines){
scroll();
y--;
}
}
else{
vidmem[(x+cols*y)*2]=c;
if(++x>=cols){
x=0;
if(++y>=lines){
scroll();
y--;
}
}
}
}
gotoxy(x,y);
}
Function copy from one part of memory to another.
Void* memcpy( void*__dest,__const void*___src,unsigned int__n)
{
int I;
char*d=(char*)dest,*s=(char*)__src;
for(I=o;I<__n,I++) d[i] =s[i]
}
Function, making long nice. Very usefull for debugging.
Make_sound()
{
__asm__("
movb $0xB6, %al
outb %al,$0x43
movb $0x0D, %al
outb %al,$0x43
movb $0x11,%al
outb %al,$0x42
inb $0x61,%al
orb $3,%al
outb %al,
outb %al,$0x61
");
}
And here is the main function.
Int start_my_kernel()
{
/*setting general parameters*/
vidmem=(char*)0xb8000;
vidport=0x3b4;
lines=25;
cots=80;
/* reading the saved cursor coordinates*/
curr_x=*(unsigned char *)(0x8000);
curr_y=*(unsigned char *)(0x80001);
/*show the following*/
puts("MiniOS Loaded/n");
/*Go into endless loop*/
while(1);
}
This is the end of your experience. Now you have your own OS.
This article was originally written by ivi |