Pascal Bare Bones
WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory? |
Difficulty level |
---|
![]() 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 <ki[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 <ki[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
- https://github.com/torokernel/torokernel/wiki/Writing-Drivers-In-Freepascal, notes about how to write drivers by using the Frepascal language.
- https://wiki.freepascal.org/System_unit, a tutorial to write a simple system unit