Barebones OS - nasm, fpc and ld
by: G.E. Ozz Nixon Jr.
Code by: De Deyn Kim
Published: March 2009
©opyright 2009 by Friends of FPC
When building your own operating system it helps if someone sheds the
light on this mysterious world ... building a kernel. After working on a few kernel projects I came
across "Barebones OS" on
OS Dev - if you follow
the steps outlined below you can quickly see - building a kernel for GRUB is almost childs play. This
may also give you the false impression that you will be able to build your OS in a few months. So,
please understand - this will show you a concept - you will need to read more of our articles and
research documents to start absorbing all of the nasty details this research document left out.
Tools you will need:
FPC of course
NASM assembler
ld with elf support
a working linux with GRUB (you will need to reboot it to start this OS!)
stub.asm
;/////////////////////////////////////////////////////////
;// //
;// Freepascal barebone OS //
;// stub.asm //
;// //
;/////////////////////////////////////////////////////////
;//
;// By: De Deyn Kim
;// License: Public domain
;//
;
; Kernel stub
;
;
; We are in 32bits protected mode
;
[bits 32]
;
; Export entrypoint
;
[global kstart]
;
; Import kernel entrypoint
;
[extern kmain]
;
; Posible multiboot header flags
;
MULTIBOOT_MODULE_ALIGN equ 1<<0
MULTIBOOT_MEMORY_MAP equ 1<<1
MULTIBOOT_GRAPHICS_FIELDS equ 1<<2
MULTIBOOT_ADDRESS_FIELDS equ 1<<16
;
; Multiboot header defines
;
MULTIBOOT_HEADER_MAGIC equ 0x1BADB002
MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_MODULE_ALIGN | MULTIBOOT_MEMORY_MAP
MULTIBOOT_HEADER_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC+MULTIBOOT_HEADER_FLAGS)
;
; Kernel stack size
;
KERNEL_STACKSIZE equ 0x4000
section .text
;
; Multiboot header
;
align 4
dd MULTIBOOT_HEADER_MAGIC
dd MULTIBOOT_HEADER_FLAGS
dd MULTIBOOT_HEADER_CHECKSUM
;
; Entrypoint
;
kstart:
mov esp, KERNEL_STACK+KERNEL_STACKSIZE ;Create kernel stack
push eax ;Multiboot magic number
push ebx ;Multiboot info
call kmain ;Call kernel entrypoint
cli ;Clear interrupts
hlt ;Halt machine
section .bss
;
; Kernel stack location
;
align 32
KERNEL_STACK:
resb KERNEL_STACKSIZE
kernel.pas
/////////////////////////////////////////////////////////
// //
// Freepascal barebone OS //
// kernel.pas //
// //
/////////////////////////////////////////////////////////
//
// By: De Deyn Kim
// License: Public domain
//
unit kernel; {note: unit not program!}
interface
uses
multiboot,
console;
procedure kmain(mbinfo: Pmultiboot_info_t; mbmagic: DWORD);
stdcall;
implementation
procedure kmain(mbinfo: Pmultiboot_info_t; mbmagic: DWORD);
stdcall; [public, alias: 'kmain'];
begin
kclearscreen();
kwritestr('Freepascal barebone OS booted!');
xpos := 0;
ypos += 1;
if (mbmagic <> MULTIBOOT_BOOTLOADER_MAGIC) then begin
kwritestr('Halting system...');
kwritestr('A multiboot-compliant boot loader needed!');
asm
cli;
hlt;
end;
end
else begin
kwritestr('Booted by a multiboot-compliant boot loader!');
xpos := 0;
ypos += 2;
kwritestr('Multiboot information:');
xpos := 0;
ypos += 2;
kwritestr(' Lower memory = ');
kwriteint(mbinfo^.mem_lower);
kwritestr('KB');
xpos := 0;
ypos += 1;
kwritestr(' Higher memory = ');
kwriteint(mbinfo^.mem_upper);
kwritestr('KB');
xpos := 0;
ypos += 1;
kwritestr(' Total memory = ');
kwriteint(((mbinfo^.mem_upper + 1000) div 1024) +1);
kwritestr('MB');
end;
asm
@loop:
jmp @loop
end;
end;
end.
console.pas
/////////////////////////////////////////////////////////
// //
// Freepascal barebone OS //
// console.pas //
// //
/////////////////////////////////////////////////////////
//
// By: De Deyn Kim
// License: Public domain
//
}
unit console;
interface
var
xpos: Integer = 0;
ypos: Integer = 0;
procedure kclearscreen();
procedure kwritechr(c: Char);
procedure kwritestr(s: PChar);
procedure kwriteint(i: Integer);
procedure kwritedword(i: DWORD);
implementation
var
vidmem: PChar = PChar($b8000);
procedure kclearscreen(); [public, alias: 'kclearscreen'];
var
i: Integer;
begin
for i := 0 to 3999 do vidmem[i] := #0;
end;
procedure kwritechr(c: Char); [public, alias: 'kwritechr'];
var
offset: Integer;
begin
if (ypos > 24) then ypos := 0;
if (xpos > 79) then xpos := 0;
offset := (xpos shl 1) + (ypos * 160);
vidmem[offset] := c;
offset += 1;
vidmem[offset] := #7;
offset += 1;
xpos := (offset mod 160);
ypos := (offset - xpos) div 160;
xpos := xpos shr 1;
end;
procedure kwritestr(s: PChar); [public, alias: 'kwritestr'];
var
offset, i: Integer;
begin
if (ypos > 24) then ypos := 0;
if (xpos > 79) then xpos := 0;
offset := (xpos shl 1) + (ypos * 160);
i := 0;
while (s[i] <> Char($0)) do begin
vidmem[offset] := s[i];
offset += 1;
vidmem[offset] := #7;
offset += 1;
i += 1;
end;
xpos := (offset mod 160);
ypos := (offset - xpos) div 160;
xpos := xpos shr 1;
end;
procedure kwriteint(i: Integer); [public, alias: 'kwriteint'];
var
buffer: array [0..11] of Char;
str: PChar;
digit: DWORD;
minus: Boolean;
begin
str := @buffer[11];
str^ := #0;
if (i < 0) then begin
digit := -i;
minus := True;
end
else begin
digit := i;
minus := False;
end;
repeat
Dec(str);
str^ := Char((digit mod 10) + Byte('0'));
digit := digit div 10;
until (digit = 0);
if (minus) then begin
Dec(str);
str^ := '-';
end;
kwritestr(str);
end;
procedure kwritedword(i: DWORD); [public, alias: 'kwritedword'];
var
buffer: array [0..11] of Char;
str: PChar;
digit: DWORD;
begin
for digit := 0 to 10 do buffer[digit] := '0';
str := @buffer[11];
str^ := #0;
digit := i;
repeat
Dec(str);
str^ := Char((digit mod 10) + Byte('0'));
digit := digit div 10;
until (digit = 0);
kwritestr(@Buffer[0]);
end;
end.
multiboot.pas
/////////////////////////////////////////////////////////
// //
// Freepascal barebone OS //
// multiboot.pas //
// //
/////////////////////////////////////////////////////////
//
// By: De Deyn Kim
// License: Public domain
//
unit multiboot;
interface
const
KERNEL_STACKSIZE = $4000;
MULTIBOOT_BOOTLOADER_MAGIC = $2BADB002;
type
Pelf_section_header_table_t = ^elf_section_header_table_t;
elf_section_header_table_t = packed record
num: DWORD;
size: DWORD;
addr: DWORD;
shndx: DWORD;
end;
Pmultiboot_info_t = ^multiboot_info_t;
multiboot_info_t = packed record
flags: DWORD;
mem_lower: DWORD; { Amount of memory available below 1mb }
mem_upper: DWORD; { Amount of memory available above 1mb }
boot_device: DWORD;
cmdline: DWORD;
mods_count: DWORD;
mods_addr: DWORD;
elf_sec: elf_section_header_table_t;
mmap_length: DWORD;
mmap_addr: DWORD;
end;
Pmodule_t = ^module_t;
module_t = packed record
mod_start: DWORD;
mod_end: DWORD;
name: DWORD;
reserved: DWORD;
end;
Pmemory_map_t = ^memory_map_t;
memory_map_t = packed record
size: DWORD;
// You can declare these two as a single
// qword if your compiler supports it
base_addr_low: DWORD;
base_addr_high: DWORD;
// And again, these can be made into a
// single qword variable
length_low: DWORD;
length_high: DWORD;
mtype: DWORD;
end;
implementation
end.
system.pas
/////////////////////////////////////////////////////////
// //
// Freepascal barebone OS //
// system.pas //
// //
/////////////////////////////////////////////////////////
//
// By: De Deyn Kim
// License: Public domain
//
unit system;
interface
type
cardinal = 0..$FFFFFFFF;
hresult = cardinal;
dword = cardinal;
integer = longint;
pchar = ^char;
implementation
end.
linker.script
ENTRY(kstart)
SECTIONS
{
.text 0x100000 :
{
text = .; _text = .; __text = .;
*(.text)
. = ALIGN(4096);
}
.data :
{
data = .; _data = .; __data = .;
*(.data)
kimage_text = .;
LONG(text);
kimage_data = .;
LONG(data);
kimage_bss = .;
LONG(bss);
kimage_end = .;
LONG(end);
. = ALIGN(4096);
}
.bss :
{
bss = .; _bss = .; __bss = .;
*(.bss)
. = ALIGN(4096);
}
end = .; _end = .; __end = .;
}
Compiling and Linking the modules
nasm -f elf stub.asm -o stub.o
fpc -Aelf -n -O3 -Op3 -Si -Sc -Sg -Xd -Rintel -Tlinux kernel.pas
ld -Tlinker.script -o kernel.obj stub.o kernel.o multiboot.o system.o console.o
The above three steps compiled the stab.asm to a linkable object, compiled the kernel.pas
file - producing kernel.ppu and kernel.o -- of course kernel.o is what we are interested in.
The final step uses the ld linker to produce the output file
kernel.obj
which consists of the "elf" format stub.o, which calls kernel.o, which is dependent upon
multiboot.o, system.o and console.o -- before I explain these files and other things
that are happening - let's modify your grub file and see this actually boot up and work!
vim /boot/grub/menu.lst
append to the end of the file the following:
title Barebones OS
root (hd0,0)
kernel /home/onixon/barebones/kernel.obj
Make sure the path is to your kernel.obj file, and then save/quit. Reboot your VM and select
the "Barebones OS" choice in the GRUB menu. You will see a screen like:
Barebones OS Screenshot in VMWare on Mac OS X inside an Ubuntu 8 VM
So, if everything went well... you too will have a simple DOS looking VM that states how much memory is available to the kernel.
Normally I would have posted this as an article, however, once you have the above working -- here comes the research.
The above code (mainly the stub.asm and kernel.pas) makes it look like sub.asm is pushing eax (MULTIBOOT_HEADER_MAGIC) from
stub.asm to kernel.pas' kmain... this is not possible. If it was we would get an error stating "A multiboot-compliant
boot loader needed!"... because the MAGIC number in stub.asm is 0x1...2 and in multiboot.pas it is defined as 0x2...2.
First step of course is to read-up on grub... doing a
grub --version
Showed me I am running 0.97 (now called GRUB Legacy) -- so I did some reading and found that Grub displays information that
kmain is erasing in the kclearscreen() call. So, I removed that line, changed ypos to start on line 10.. and recompiled then
rebooted to the new kernel - to see ", <0x100000:0x1000:0x0>, <0x101000,0x1000:0x4000>, shtab=0x106168Starting up..." then
the normal Barebones introduction. Then I came across "Multiboot Specification v0.6.95" which states the layout of the
Multiboot loader as:
offset type fieldname note
0 u32 magic required
4 u32 flags required
8 u32 checksum required (magic+flags)
12 u32 header address if flags[16] is set
16 u32 load address if flags[16] is set
20 u32 load end address if flags[16] is set
24 u32 bss end address if flags[16] is set
28 u32 entry address if flags[16] is set
32 u32 mode type if flags[2] is set
36 u32 width if flags[2] is set
40 u32 height if flags[2] is set
44 u32 depth if flags[2] is set
Then I found what I was looking for...
EAX contains 0x2BADB002 - denoting to the
operating system that it was loaded by a multiboot-compliant boot loader (in this case GRUB).
EBX
contains the 32bit address of the multiboot information structure provided by the boot loader. The structure of this multiboot
information is:
+-------------------+
0 | flags | (required)
+-------------------+
4 | mem_lower | (present if flags[0] is set)
8 | mem_upper | (present if flags[0] is set)
+-------------------+
12 | boot_device | (present if flags[1] is set)
+-------------------+
16 | cmdline | (present if flags[2] is set)
+-------------------+
20 | mods_count | (present if flags[3] is set)
24 | mods_addr | (present if flags[3] is set)
+-------------------+
28 - 40 | syms | (present if flags[4] or
| | flags[5] is set)
+-------------------+
44 | mmap_length | (present if flags[6] is set)
48 | mmap_addr | (present if flags[6] is set)
+-------------------+
52 | drives_length | (present if flags[7] is set)
56 | drives_addr | (present if flags[7] is set)
+-------------------+
60 | config_table | (present if flags[8] is set)
+-------------------+
64 | boot_loader_name | (present if flags[9] is set)
+-------------------+
68 | apm_table | (present if flags[10] is set)
+-------------------+
72 | vbe_control_info | (present if flags[11] is set)
76 | vbe_mode_info |
80 | vbe_mode |
82 | vbe_interface_seg |
84 | vbe_interface_off |
86 | vbe_interface_len |
+-------------------+