Pascal Bare Bones

From OSDev Wiki
Jump to: navigation, search

WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory?

Difficulty level
Difficulty 2.png
Medium

Tools needed to build the project:

  • FPC (You'll need the i386 version as that's the architecture we're targeting)
  • NASM
  • binutils(ld) built for 32-bit elf support

Contents

stub.asm

;/////////////////////////////////////////////////////////
;//                                                     //
;//               Freepascal barebone OS                //
;//                      stub.asm                       //
;//                                                     //
;/////////////////////////////////////////////////////////
;//
;//     By:             De Deyn Kim <[email protected]>
;//     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 <[email protected]>
//      License:        Public domain
//
}
 
unit kernel;
 
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, 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 <[email protected]>
//      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(str);
end;
 
end.

multiboot.pas

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 one qword variable. }
          length_low: DWORD;
          length_high: DWORD;
          mtype: DWORD;
        end;
 
implementation
 
end.

system.pas

Since fpc-3.2.0 there was added necessary parts (in code block that part was highlighted by comment lines), because without them developer could met error like:

system.pas(18,1) (system)   Parsing implementation of SYSTEM
system.pas(18,1) Fatal: Internal type "TEXCEPTADDR" was not found. Check if you use the correct run time library.
Fatal: Compilation aborted

Valid version of system.pas on 2022/12/29:

unit system;
 
{$MODE FPC}
 
interface
 
  type
    cardinal = 0..$FFFFFFFF;
    hresult = cardinal;
    dword = cardinal;
    integer = longint;
 
    pchar = ^char;
 
    { That part comes from fpc 3.2.0 as nessesary}
    TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkSet,
    tkMethod, tkSString, tkLString, tkAString, tkWString, tkVariant, tkArray,
    tkRecord, tkInterface, tkClass, tkObject, tkWChar, tkBool, tkInt64, tkQWord,
    tkDynArray, tkInterfaceRaw, tkProcVar, tkUString, tkUChar, tkHelper, tkFile,
    tkClassRef, tkPointer);
 
    jmp_buf = packed record
      rbx, rbp, r12, r13, r14, r15, rsp, rip: QWord;
      {$IFDEF win64}
      rsi, rdi: QWord;
      xmm6, xmm7, xmm8, xmm9, xmm10, xmm11, xmm12, xmm13, xmm14, xmm15: record 
        m1, m2: QWord;
      end;
 
      mxcsr: LongWord;
      fpucw: word;
      padding: word;
      {$ENDIF win64}
    end;
 
    Pjmp_buf = ^jmp_buf;
    PExceptAddr = ^TExceptAddr;
    TExceptAddr = record 
      buf: Pjmp_buf;
      next: PExceptAddr;
      {$IFDEF CPU16}
      frametype: SmallInt;
      {$ELSE CPU16}
      frametype: LongInt;
      {$ENDIF CPU16}
    end;
 
    PGuid = ^TGuid;
    TGuid = packed record
      case Integer of
      1:
      (Data1: DWord;
        Data2: word;
        Data3: word;
        Data4: array [0 .. 7] of byte;
      );
      2:
      (D1: DWord;
        D2: word;
        D3: word;
        D4: array [0 .. 7] of byte;
      );
      3:
      ( { uuid fields according to RFC4122 }
        time_low: DWord; // The low field of the timestamp
        time_mid: word; // The middle field of the timestamp
        time_hi_and_version: word;
        // The high field of the timestamp multiplexed with the version number
        clock_seq_hi_and_reserved: byte;
        // The high field of the clock sequence multiplexed with the variant
        clock_seq_low: byte; // The low field of the clock sequence
        node: array [0 .. 5] of byte; // The spatially unique node identifier
      );
    end;
 
{ --- End of nessesary part --- }
 
implementation
 
end.

Linker script

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

Assemble stub.asm with:

nasm -f elf32 stub.asm -o stub.o
  • -f elf32 - needed exact under x86_64 systems to make correct object file

The Pascal modules with:

fpc -Aelf -n -O3 -Op3 -Si -Sc -Sg -Xd -CX -XXs -Pi386 -Rintel -Tlinux kernel.pas
  • -Aelf - instructs the internal fpc assembler to output an ELF object.;
  • -n - ignores fpc.cfg;
  • -O3 - perform level 3 optimizations;
  • -Op3 - tune code for PentiumPro / P-II / Cyrix 6x86 / K6 (TM);
  • -Si - enable C++ style INLINE keyword;
  • -Sc - enable C style operators;
  • -Sg - enable support for labels;
  • -Xd - tells the compiler to forget the standard library path;
  • -CX - tells the compiler to create smartlinkable units
  • -XXs - tells the compiler to do smartlinking (-XX) and debugging symbols stripping (-Xs)
  • -Pi386 - tells the compiler to force i386 mode (actual for x86_64 host systems)
  • -Rintel - sets the inline assembly syntax to intel style;
  • -Tlinux - specifies that the target operating system is Linux. (Provides a sensible system unit to use)

Then link the whole thing with:

i386-elf-ld --gc-sections -s -Tlinker.script -o kernel.obj stub.o kernel.o multiboot.o system.o console.o
  • --gc-sections -s, in combination with -CX -XXs above, eliminates RTTI symbols from resulting binary

In case of trouble linking under x86_64 system try this line:

i686-elf-ld -A elf-386 --gc-sections -s -Tlinker.script -o kernel.obj stub.o kernel.o multiboot.o system.o console.o

Special sutuation: last time after building binutils I've got error

i386-linux-ld --gc-sections -s -Tlinker.script -o kernel.obj stub.o kernel.o multiboot.o console.o system.o
ld: i386 architecture of input file `stub.o' is incompatible with i386:x86-64 output
ld: i386 architecture of input file `kernel.o' is incompatible with i386:x86-64 output
ld: i386 architecture of input file `multiboot.o' is incompatible with i386:x86-64 output
ld: i386 architecture of input file `console.o' is incompatible with i386:x86-64 output
ld: i386 architecture of input file `system.o' is incompatible with i386:x86-64 output
make: *** [Makefile:27: _LD] Error 1

That type of error can encounter if i386-linux-ld have wrong content. In my situation file content edited to:

#!/bin/bash
/full/path/to/compiled/cross/binutils/ld-new -A elf32-i386 $@

makeiso.sh

Also a good option is create a bootable ISO file to make run it with VirtualBox or qemu:

#!/bin/sh
TMPISO=iso
TMPBOOT=${TMPISO}/boot
TMPGRUB=${TMPBOOT}/grub
TMPCFG=${TMPGRUB}/grub.cfg
 
mkdir $TMPISO
mkdir $TMPBOOT
mkdir $TMPGRUB
cp kernel.obj $TMPBOOT/kernel.obj
echo 'set timeout=0'                  > $TMPCFG
echo 'set default =0'                >> $TMPCFG
echo ''                              >> $TMPCFG
echo 'menuentry "Pascal bare" {'     >> $TMPCFG
echo '  multiboot /boot/kernel.obj'  >> $TMPCFG
echo '  boot'                        >> $TMPCFG
echo '}'                             >> $TMPCFG
grub-mkrescue --output=pascal-kernel.iso iso
rm -rf $TMPISO

And simple run:

qemu-system-i386 pascal-kernel.iso

Alternative compiling: Makefile

Accumulating the lines from previous part we can make a Makefile:

# Freepascal BareboneOS 
# Makefile 
# 			2019
# by:		furaidi <[email protected]>
# License:	Public domain
 
NASMPARAMS = -f elf32 -o stub.o
LDPARAMS = -A elf32-i386 --gc-sections -s -Tlinker.script -o kernel.obj
FPCPARAMS = -Aelf -n -O2 -Op3 -Si -Sc -Sg -Xd -CX -XXs -Pi386 -Rintel -Tlinux
TMPISO = iso
TMPBOOT = $(TMPISO)/boot
TMPGRUB = $(TMPBOOT)/grub
TMPCFG  = $(TMPGRUB)/grub.cfg
 
objects = stub.o kernel.o multiboot.o console.o system.o
 
_FPC:
	@echo 'Compile kernel'
	fpc $(FPCPARAMS) kernel.pas
 
_NASM:
	@echo 'Compile stub'
	nasm $(NASMPARAMS) stub.asm
 
_LD:
	@echo 'Link them together'
	i386-linux-ld $(LDPARAMS) $(objects)
 
all: _FPC _NASM _LD
 
install:
	mkdir $(TMPISO)
	mkdir $(TMPBOOT)
	mkdir $(TMPGRUB)
	cp kernel.obj $(TMPBOOT)/kernel.obj
	echo 'set timeout=0'     		 > $(TMPCFG)
	echo 'set default =0'		        >> $(TMPCFG)
	echo ''                      		>> $(TMPCFG)
	echo 'menuentry "Pascal Bare" {'	>> $(TMPCFG)
	echo '  multiboot /boot/kernel.obj'  	>> $(TMPCFG)
	echo '  boot'              		>> $(TMPCFG)
	echo '}'                      		>> $(TMPCFG)
	grub-mkrescue --output=pascal-kernel.iso $(TMPISO)
	rm -rf $(TMPISO)
 
clean:
	rm -rf $(TMPISO)
	rm -f *.o
	rm -f *.ppu

Further Steps

After when bare bones are ready and tested, you'd probably want to go with a few ways separately or combine:

  • Add terminal workaround. Pure *nix way like changing terminals, built-in commands, simple user management
  • Add graphic interface from start. That is Windows NT family way. You need to add SVGA-like drivers and/or mouse input and everything you need (or thinks so)

Also you may want to adapt *nix libraries such as binutils, make build target for compiling your fpc programs for your new os like i386-myos. From that point you are on your own.

See Also

External Links

Personal tools
Namespaces
Variants
Actions
Navigation
About
Toolbox