GitFOSS
refactor: make code more readable/maintainable
+ 739
- 105
@@ -1,4 +1,5 @@
 ; void reboot()
+global reboot
 reboot:
 	.notify:
 		mov si, rebootMessage

file deleted
build_image.sh
@@ -1,51 +0,0 @@
-#!/bin/bash
-
-# Flags
-
-CC_FLAGS="-ffreestanding -nostdinc -nostdinc++ \
-					-Wall -Wextra \
-					-o ./bin/kernel.bin -target i386-pc-none-elf \
-					-I ./kernel/"
-LD_FLAGS="-nostdlib -Wl,--oformat=binary,-T./kernel/linker.ld"
-CPP_FILES="./kernel/unityBuild.cpp ./build/kernelEntryPoint.o"
-
-# Build
-
-mkdir {bin,build} 2> /dev/null
-
-echo -e "Cleaning..."
-rm *.bin *.img *.iso *.o *.vmdk 2> /dev/null
-
-echo -e "\nBuilding..."
-nasm ./boot/bootloader.asm -I ./boot/ -o ./bin/bootloader.bin || exit 1
-nasm -felf32 ./kernel/entry.asm -o ./build/kernelEntryPoint.o || exit 1
-
-use_crystal_kernel=${USE_CRYSTAL_KERNEL:-true}
-
-if [ "$use_crystal_kernel" = true ]
-then
-	# Build crystal kernel without stdlib, using the custom prelude, and targeting i386-unknown-linux-elf
-	crystal build \
-		--no-debug -Dkernel \
-		--cross-compile --target i386-unknown-linux-elf \
-		--prelude=../prelude.cr \
-		-o ./build/kernel ./kernel/kernel.cr
-
-	# Link the kernel into an ELF executable, then convert it to a flat binary for the bootloader.
-	ld -m elf_i386 -nostdlib -T ./kernel/linker.ld -o ./bin/kernel.elf ./build/kernelEntryPoint.o ./build/kernel.o || exit 1
-	objcopy -O binary ./bin/kernel.elf ./bin/kernel.bin || exit 1
-else
-	clang++ $CC_FLAGS $LD_FLAGS $CPP_FILES || exit 1
-fi
-
-cp ./bin/kernel.bin ./iso/boot/kernel.bin || exit 1
-
-./tools/genVDisk --output "crystalos.img" --floppy \
-				  --bootloader ./bin/bootloader.bin --kernel ./bin/kernel.bin
-
-# Run
-
-echo -e "\nRunning..."
-qemu-system-i386 -drive if=floppy,index=0,format=raw,file=crystalos.img || exit 1
-
-exit 0

@@ -1,4 +1,50 @@
 
 def say_hello
   puts "Hello from Crystal!"
-end
+end
+
+
+# def outb(port : UInt16, value : UInt8)
+#   asm (%(outb %al, %dx) : : "a"(value), "Nd"(port))
+# end
+
+# def set_cursor_position(row : Int32, col : Int32)
+#   position = (80 * row + col).to_u16
+
+#   # # Write the high byte
+#   # outb(0x3D4_u16, 0x0E_u8);
+#   # outb(0x3D5_u16, ((position >> 8) & 0xFF).to_u8);
+
+#   # # Write the low byte
+#   # outb(0x3D4_u16, 0x0F_u8);
+#   # outb(0x3D5_u16, (position & 0xFF).to_u8);
+# end
+
+# def clear_screen
+#   video_memory = Pointer(UInt8).new(0xB8000_u64)
+#   screen_size = 80 * 25
+#   i = 0
+#   while i < screen_size
+#     (video_memory + (i * 2)).value = " ".to_unsafe.value
+#     (video_memory + (i * 2) + 1).value = 0x07_u8
+#     i += 1
+#   end
+# end
+
+# message = "[CrystalOS Kernel] Hello from Crystal"
+  
+# clear_screen
+# set_cursor_position(0, 0)
+
+# _str : Pointer(UInt8) = message.to_unsafe
+# video_memory = Pointer(UInt8).new(0xB8000_u64)
+# i = 0
+# ch = _str.value
+# while ch > 0
+#   ch = (_str + i).value
+#   break if ch == 0
+#   (video_memory + (i * 2)).value = ch
+#   (video_memory + (i * 2) + 1).value = 0xB0_u8
+#   i += 1
+# end
+  

new file
kernel/boot.cr
@@ -0,0 +1,18 @@
+module Boot
+  # Kernel entry point (converted from main.c's kmain)
+  def self.kmain : Nil
+    # Initialize core kernel subsystems in order
+    IDT.init
+    Timer.init
+    PS2.init
+    VGA.init
+
+    # Enable hardware interrupts
+    asm("sti")
+
+    # Main kernel loop (halt until next interrupt)
+    loop do
+      asm("hlt")
+    end
+  end
+end

new file
kernel/idt.cr
@@ -0,0 +1,55 @@
+require "./vga.cr"
+
+# IDT module
+module IDT
+  # require "crystal/kernel"  # core types
+
+  IDT_ENTRIES = 256
+
+  # Storage for the IDT entries
+  @idt = StaticArray(Entry, IDT_ENTRIES)
+
+  @[Packed]
+  struct Entry
+    offset_low : UInt16
+    selector : UInt16
+    ist : UInt8
+    type_attr : UInt8
+    offset_mid : UInt16
+    offset_high : UInt32
+    zero : UInt32
+  end
+
+  @[Packed]
+  struct Ptr
+    limit : UInt16
+    base : UInt64
+  end
+
+  # External functions from assembly
+  extern "C" def lidt(ptr : Ptr*): Nil
+    {% if flag?(:linux) %}
+    {% else %}
+    {% end %}
+  end
+
+  # IRQ handlers storage
+  @irq_handlers = StaticArray(Void*, IDT_ENTRIES)
+
+  extern "C" def isr_handler_common: Nil
+
+  def self.register_irq(irq : Int32, handler : Void*) : Nil
+    if irq >= 0 && irq < IDT_ENTRIES
+      @irq_handlers[irq] = handler
+    end
+  end
+
+  def self.init : Nil
+    ptr = Ptr.new
+    ptr.limit = (IDT_ENTRIES * Entry.size) - 1
+    ptr.base = getaddr(&@idt).to_u64
+    lidt(&ptr)
+
+    VGA.putd("IDT initialized\n")
+  end
+end

@@ -1,3 +1,8 @@
+require "./tiny_alloc_i386.cr"
+require "./vga.cr"
+# require "./pmm.cr"
+# require "./ps2.cr"
+
 lib LibBootstrap
   @[Packed]
   struct StartInfo

...
@@ -7,64 +12,36 @@ lib LibBootstrap
 end
 
 fun kearly(info_ptr: LibBootstrap::StartInfo*)
-
   # Get the startup info
   info = info_ptr.value
-
-  # Initialize stuff
-  # GDT.init
-  # Heap.init info.end_of_kernel
-  # PIC.remap
-  # PIC.enable
-  # IDT.init
-end
-
-def outb(port : UInt16, value : UInt8)
-  asm (%(outb %al, %dx) : : "a"(value), "Nd"(port))
-end
-
-def set_cursor_position(row : Int32, col : Int32)
-  position = (80 * row + col).to_u16
-
-  # # Write the high byte
-  # outb(0x3D4_u16, 0x0E_u8);
-  # outb(0x3D5_u16, ((position >> 8) & 0xFF).to_u8);
-
-  # # Write the low byte
-  # outb(0x3D4_u16, 0x0F_u8);
-  # outb(0x3D5_u16, (position & 0xFF).to_u8);
-end
-
-def clear_screen
-  video_memory = Pointer(UInt8).new(0xB8000_u64)
-  screen_size = 80 * 25
-  i = 0
-  while i < screen_size
-    (video_memory + (i * 2)).value = " ".to_unsafe.value
-    (video_memory + (i * 2) + 1).value = 0x07_u8
-    i += 1
-  end
 end
 
 fun kmain
-  message = "[CrystalOS Kernel] Hello from Crystal"
+  # PMM.init # Physical Memory Init
+  # TinyAllocI386.init # Tiny Bump Allocator
+
+  VGA.init
+
+  VGA.puts(
+    "[CrystalOS Kernel] Hello from Crystal.",
+    VGA::Colors::BLACK_ON_CYAN,
+  )
+  VGA.puts("\n")
+  VGA.puts("Hello world!\nWorks maybe?\n", line: 1)
+  VGA.puts("Can print on multiple lines\n", line: 2)
+  VGA.puts("But because we don't have a Heap, we need to set lines number in puts call for now...\n", line: 3)
+  # VGA.puts(%(
+  #   Works well
+  #   multiline buffer?
+  # ),
+  #   VGA::Colors::BLACK_ON_LIME,
+  # )
+  # VGA.puts("\n\n\n")
+  # VGA.puts "hello world\n"
   
-  clear_screen
-  set_cursor_position(0, 0)
-
-  _str : Pointer(UInt8) = message.to_unsafe
-  video_memory = Pointer(UInt8).new(0xB8000_u64)
-  i = 0
-  ch = _str.value
-  while ch > 0
-    ch = (_str + i).value
-    break if ch == 0
-    (video_memory + (i * 2)).value = ch
-    (video_memory + (i * 2) + 1).value = 0xB0_u8
-    i += 1
-  end
+  #VGA.dump_cursor_and_cells
 
   while true
+    asm (%(hlt))
   end
 end
-

new file
kernel/kernel_api.cr
@@ -0,0 +1,8 @@
+module KernelAPI
+  # Kernel module structure (converted from module.h)
+  struct KernelModule
+    name : UInt8*      # Module name (null-terminated string)
+    init : -> Nil      # Module initialization function
+    uninit : -> Nil    # Module cleanup function
+  end
+end

new file
kernel/pmm.cr
@@ -0,0 +1,57 @@
+# PMM module - Physical Memory Manager
+module PMM
+  PAGE_SIZE = 4096
+  KERNEL_HEAP_START = 0x200000_u64
+  KERNEL_HEAP_SIZE = 16 * 1024 * 1024
+
+  TOTAL_PAGES = 40322 # KERNEL_HEAP_SIZE / PAGE_SIZE
+
+  @@page_bitmap = StaticArray(UInt64, 40322).new()
+  @@heap_ptr = Pointer(UInt8).new(KERNEL_HEAP_START)
+  @@heap_end = Pointer(UInt8).new(KERNEL_HEAP_START + KERNEL_HEAP_SIZE)
+
+  def self.init : Nil
+    TOTAL_PAGES.times do |i|
+      clear_page_used(i.to_u64)
+    end
+  end
+
+  private def self.set_page_used(page : UInt64) : Nil
+    @@page_bitmap[page // 64] |= (1_u64 << (page % 64))
+  end
+
+  private def self.clear_page_used(page : UInt64) : Nil
+    @@page_bitmap[page // 64] &= ~(1_u64 << (page % 64))
+  end
+
+  private def self.is_page_used(page : UInt64) : Bool
+    ((@@page_bitmap[page // 64] >> (page % 64)) & 1_u64) == 1_u64
+  end
+
+  def self.alloc_page : Pointer(Void)?
+    TOTAL_PAGES.times do |i|
+      unless is_page_used(i)
+        set_page_used(i)
+        return Pointer(Void).new(KERNEL_HEAP_START + i * PAGE_SIZE)
+      end
+    end
+    nil
+  end
+
+  def self.free_page(page : Pointer(Void)) : Nil
+    addr = page.address
+    return if addr < KERNEL_HEAP_START || addr >= KERNEL_HEAP_START + KERNEL_HEAP_SIZE
+    page_idx = (addr - KERNEL_HEAP_START) // PAGE_SIZE
+    clear_page_used(page_idx)
+  end
+
+  def self.kalloc(bytes : UInt64) : Pointer(Void)?
+    size = (bytes + 7) & ~7
+    ret = @@heap_ptr
+    if @@heap_ptr + size > @@heap_end
+      return nil
+    end
+    @@heap_ptr += size
+    ret
+  end
+end

new file
kernel/ps2.cr
@@ -0,0 +1,11 @@
+require "./vga.cr"
+
+# PS2 module - Keyboard/Mouse driver
+module PS2
+
+  def self.init : Nil
+    # Initialize PS/2 controller
+    VGA.puts("PS/2 initialized\n")
+  end
+
+end

new file
kernel/timer.cr
@@ -0,0 +1,18 @@
+require "./vga.cr"
+
+# Timer module - System timer
+module Timer
+  TIMER_FREQ = 1000_u32
+
+  # External assembly ISR for timer
+  fun timer_isr_common : Void
+
+  def self.init : Nil
+    # Set up PIT or HPET here (placeholder)
+    VGA.puts("Timer initialized\n")
+  end
+
+  def self.enable : Nil
+    # Enable timer IRQ
+  end
+end

new file
kernel/tiny_alloc_i386.cr
@@ -0,0 +1,136 @@
+# Minimal bump allocator + memset/memcpy for i386 bare-metal Crystal
+
+# Exports C symbols: malloc, free, memset, memcpy, calloc
+
+# 32-bit addresses/sizes (see linker.ld)
+HEAP_START = 0x00108000_u32
+HEAP_SIZE  = 0x00008000_u32
+
+struct HeapState
+  @heap_base : UInt32 = HEAP_START
+  @heap_top  : UInt32 = HEAP_START + HEAP_SIZE
+  @heap_ptr  : UInt32 = HEAP_START
+  @inited    : Bool   = false
+  
+  def heap_base
+    @heap_base
+  end
+  def heap_base=(v : UInt32)
+    @heap_base = v
+  end
+  
+  def heap_top
+    @heap_top
+  end
+  def heap_top=(v : UInt32)
+    @heap_top = v
+  end
+  
+  def heap_ptr
+    @heap_ptr
+  end
+  def heap_ptr=(v : UInt32)
+    @heap_ptr = v
+  end
+  
+  def inited
+    @inited
+  end
+  def inited=(v : Bool)
+    @inited = v
+  end
+end
+
+# top-level var binding (becomes a global/static)
+HEAP_STATE = HeapState.new
+
+module TinyAllocI386
+  def self.init
+    return if HEAP_STATE.inited
+    HEAP_STATE.heap_ptr = HEAP_STATE.heap_base
+    HEAP_STATE.inited = true
+  end
+
+  def self.align_up32(v : UInt32, align : UInt32) : UInt32
+    mask = align - 1_u32
+    (v + mask) & ~mask
+  end
+
+  def self.malloc32(size : UInt32, align : UInt32 = 8_u32) : Pointer(UInt8)?
+    init_if_needed
+    return nil if size == 0_u32
+    a = align_up32(HEAP_STATE.heap_ptr, align)
+    new_ptr = a + size
+    if new_ptr > HEAP_STATE.heap_top
+      return nil
+    end
+    HEAP_STATE.heap_ptr = new_ptr
+    Pointer(UInt8).new(a.to_u64)
+  end
+
+  def self.reset_heap
+    HEAP_STATE.heap_ptr = HEAP_STATE.heap_base
+  end
+
+  def self.memset_bytes(ptr : Pointer(UInt8), c : UInt8, n : UInt32) : Pointer(UInt8)
+    i = 0_u32
+    while i < n
+      (ptr + i).value = c
+      i += 1
+    end
+    ptr
+  end
+
+  def self.memcpy_bytes(dst : Pointer(UInt8), src : Pointer(UInt8), n : UInt32) : Pointer(UInt8)
+    i = 0_u32
+    while i < n
+      (dst + i).value = (src + i).value
+      i += 1
+    end
+    dst
+  end
+
+  def self.calloc32(nmemb : UInt32, size : UInt32, align : UInt32 = 8_u32) : Pointer(UInt8)?
+    total = nmemb * size
+    p = malloc32(total, align)
+    return nil if p.nil?
+    memset_bytes(p, 0_u8, total)
+    p
+  end
+end
+
+# Expose C symbols via a lib block (no @[Export] needed)
+lib CExports
+  fun malloc(size : UInt32) : Pointer(Void)
+  fun free(ptr : Pointer(Void))
+  fun memset(ptr : Pointer(Void), c : Int32, n : UInt32) : Pointer(Void)
+  fun memcpy(dest : Pointer(Void), src : Pointer(Void), n : UInt32) : Pointer(Void)
+  fun calloc(nmemb : UInt32, size : UInt32) : Pointer(Void)
+end
+
+# Providd implementations
+def malloc(size : UInt32) : Pointer(Void)
+  TinyAllocI386.malloc32(size).as(Pointer(Void))
+end
+
+def free(ptr : Pointer(Void))
+  # no-op
+end
+
+def memset(ptr : Pointer(Void), c : Int32, n : UInt32) : Pointer(Void)
+  TinyAllocI386.memset_bytes(Pointer(UInt8).new(ptr.to_i), c.to_u8, n)
+  ptr
+end
+
+def memcpy(dest : Pointer(Void), src : Pointer(Void), n : UInt32) : Pointer(Void)
+  TinyAllocI386.memcpy_bytes(
+    Pointer(UInt8).new(dest.to_i),
+    Pointer(UInt8).new(src.to_i),
+    n
+  )
+  dest
+end
+
+def calloc(nmemb : UInt32, size : UInt32) : Pointer(Void)
+  TinyAllocI386.calloc32(nmemb, size).as(Pointer(Void))
+end

new file
kernel/utility-inl.asm
@@ -0,0 +1,124 @@
+bootDriveNumber: db 0
+
+diskErrorMessage: db "Error: Failed to read disk!", CR, LF, 0
+rebootMessage: db "Press any key to reboot...", CR, LF, 0
+
+; void reboot()
+global reboot
+reboot:
+	.notify:
+		mov si, rebootMessage
+		call print_string
+
+	.wait_for_key_press:
+		xor ax, ax
+		int 0x16
+
+	.restart:
+		jmp word 0xffff:0x0000
+
+; void clear_screen()
+clear_screen:
+	.clear:
+		mov ax, 0x0700		; Entire screen
+		mov bx, 0x07			; Colour (black background, white foreground)
+		xor cx, cx				; Top-left of screen is (0, 0)
+		mov dx, 0x184f		; Screen size: 24 rows x 79 columns
+		int 0x10
+
+	.move_cursor:
+		mov ax, 0x02
+		xor dx, dx				; Move to (0, 0)
+		xor bh, bh				; Page 0
+		int 0x10
+
+	.cleanup:
+		ret
+
+; void print_string(ds:si string)
+print_string:
+	.print_char:
+		; Gets a character and compares it with NULL
+		mov al, [ds:si]
+		cmp al, 0
+		je .done
+
+		; Calls the interuupt to print a character
+		mov ah, 0x0e
+		xor bx, bx
+		int 0x10
+
+		; Move to the next character
+		inc si
+		jmp .print_char
+
+	.done:
+		ret
+
+; void read_disk(cx sector, al number-to-read, es:bx into)
+read_disk:
+	.read:
+		push ax
+		push bx
+		call calculate_chs
+		pop bx
+		pop ax
+
+		mov dl, [bootDriveNumber]
+		mov ah, 0x02
+		int 0x13
+
+		jc .read_failed
+		ret
+
+	.read_failed:
+		mov si, diskErrorMessage
+		call print_string
+		call reboot
+
+; (ch cylinder, cl sector, dh head) calculate_chs(cx LBA-sector)
+calculate_chs:
+	sectorsPerTrack: equ 36
+	headsPerCylinder: equ 2
+
+	.calculate_sector:
+		xor dx, dx
+		mov ax, cx
+		mov bx, sectorsPerTrack
+		div bx					; LBA div/mod SPT
+		inc dx
+		mov [tempSector], dl
+
+	.calculate_head:
+		; ax already contains quotient of LBA / SPT
+		xor dx, dx
+		mov bx, headsPerCylinder
+		div bx
+		mov [tempHead], dl
+
+	.calculate_cylinder:
+		xor dx, dx
+		mov ax, cx
+		mov bx, sectorsPerTrack * headsPerCylinder
+		div bx
+		mov [tempCylinder], ax
+
+	.finish:
+		; cx:       -- CH -- -- CL --
+		; Cylinder: 76543210 98
+		; Sector:              543210
+		movzx cx, byte [tempSector]
+		mov ax, word [tempCylinder]
+		shl ax, 8
+		or cx, ax
+		mov ax, word [tempCylinder]
+		and ax, 0xc000
+		shr ax, 8
+		or cx, ax
+		mov dh, byte [tempHead]
+
+		ret
+
+tempCylinder: dw 0
+tempHead: db 0
+tempSector: db 0

new file
kernel/vga.cr
@@ -0,0 +1,148 @@
+struct VGACursor
+  col : Int32 = 0
+  row : Int32 = 0
+  
+  def initialize
+    @col = 0
+    @row = 0
+  end
+  
+  def col : Int32
+    @col
+  end
+  def col=(c : Int32)
+    @col = c
+  end
+  def col_incr(c : Int32)
+    @col += c
+  end
+  
+  def row : Int32
+    @row
+  end
+  def row=(r : Int32)
+    @row = r
+  end
+  def row_incr(r : Int32)
+    @row += r
+  end
+end
+
+module VGA
+  # VGA text mode buffer address (0xB8000)
+  VGA_ADDRESS = 0xB8000_u64
+  VGA_BUFFER = Pointer(UInt8).new(VGA_ADDRESS)
+
+  # Buffer size
+  VGA_WIDTH = 80
+  VGA_HEIGHT = 25
+  VGA_SIZE = VGA_WIDTH * VGA_HEIGHT
+  
+  # Cursor state
+  VGA_CURSOR = VGACursor.new
+
+  # Default color: white text on black background
+  DEFAULT_COLOR = 0x07_u8
+  
+  module Colors
+    DEFAULT       = 0x07_u8
+    BLACK_ON_CYAN = 0xB0_u8
+    BLACK_ON_LIME = 0xA0_u8
+  end
+
+  # Initialize VGA subsystem (clear screen)
+  def self.init : Nil
+    clear
+  end
+
+  # Clear the entire VGA screen
+  def self.clear : Nil
+    i = 0
+    while i < VGA_SIZE
+      (VGA_BUFFER + (i * 2)).value = " ".to_unsafe.value
+      (VGA_BUFFER + (i * 2) + 1).value = Colors::DEFAULT
+      i += 1
+    end
+    VGA_CURSOR.col = 0
+    VGA_CURSOR.row = 0
+  end
+
+  # Write a single character to the current cursor position
+  def self.putchar(
+    ch : UInt8,
+    i : Int32? = 0,
+    line : Int32? = 0,
+    color : UInt8? = Colors::DEFAULT
+  ) : Nil
+    VGA_CURSOR.col = i
+    VGA_CURSOR.row = line
+  
+    # Simplified cursor handling (replace with actual cursor logic later)
+    if ch == ("\n".to_unsafe).value # 0x0A_u8 # \n
+      VGA_CURSOR.col = 0
+      VGA_CURSOR.row += 1
+      if VGA_CURSOR.row >= VGA_HEIGHT
+        VGA_CURSOR.row = VGA_HEIGHT - 1
+      end
+      return
+    end
+
+    offset = (VGA_CURSOR.row * VGA_WIDTH + VGA_CURSOR.col) * 2
+
+    (VGA_BUFFER + offset).value = ch
+    (VGA_BUFFER + offset + 1).value = color
+    
+    VGA_CURSOR.col += 1
+    
+    if VGA_CURSOR.col >= VGA_WIDTH
+      VGA_CURSOR.col = 0
+      VGA_CURSOR.row += 1
+      
+      if VGA_CURSOR.row >= VGA_HEIGHT
+        VGA_CURSOR.row = VGA_HEIGHT - 1
+      end
+    end
+
+    # Wrap around if past screen
+    # i %= VGA_SIZE
+  end
+
+  # Write a string to VGA
+  def self.puts(
+    str : String,
+    color : UInt8? = Colors::DEFAULT,
+    line : Int32? = 0,
+  ) : Nil
+    i = 0
+    while i < str.size - 1
+      ch = (str.to_unsafe + i).value
+      putchar(ch, i, line, color)
+      i += 1
+    end
+  end
+  
+  def self.debug_write_at(r : Int32, c : Int32, s : String, color : UInt8 = Colors::DEFAULT)
+    off = ((r * VGA_WIDTH) + c) * 2
+    i = 0
+    ptr = s.to_unsafe
+    while i < s.bytesize
+      (VGA_BUFFER + off + (i * 2)).value = (ptr + i).value
+      (VGA_BUFFER + off + (i * 2) + 1).value = color
+      i += 1
+    end
+  end
+
+  def self.dump_cursor_and_cells
+    # Write cursor state at row 2 col 0
+    # debug_write_at(2, 0, ["CURSOR R:",VGA_CURSOR.row," C:",VGA_CURSOR.col].join(""), Colors::BLACK_ON_CYAN)
+    # Also copy first 32 bytes of VGA buffer to row 3 for inspection
+    i = 0
+    while i < 32
+      ch = (VGA_BUFFER + (i * 2)).value
+      (VGA_BUFFER + ((3 * VGA_WIDTH + i) * 2)).value = ch
+      (VGA_BUFFER + ((3 * VGA_WIDTH + i) * 2) + 1).value = Colors::BLACK_ON_LIME
+      i += 1
+    end
+  end
+  
+end

@@ -22,4 +22,6 @@ require "./prelude/crystal_core/range"
 require "./prelude/crystal_core/string"
 require "./prelude/crystal_core/reference"
 require "./prelude/crystal_core/static_array"
-require "./prelude/puts"
+
+# crystal api
+require "./prelude/puts"

@@ -1,9 +1,11 @@
 fun print_string(str : UInt8*)
 end
 
+VGA_ADDRESS = 0xb8000_u64
+
 def puts(str : String)
   _str : Pointer(UInt8) = str.to_unsafe
-  video_memory = Pointer(UInt8).new(0xB8000_u64)
+  video_memory = Pointer(UInt8).new(VGA_ADDRESS)
   i = 0
   ch = _str.value
   while ch > 0

@@ -0,0 +1,75 @@
+#!/bin/bash
+
+# Flags
+
+CC_FLAGS="-ffreestanding -nostdinc -nostdinc++ \
+					-Wall -Wextra \
+					-o ./bin/kernel.bin -target i386-pc-none-elf \
+					-I ./kernel/"
+LD_FLAGS="-nostdlib -Wl,--oformat=binary,-T./kernel/linker.ld"
+CPP_FILES="./kernel/unityBuild.cpp ./build/kernelEntryPoint.o"
+
+# Build
+
+mkdir {bin,build} 2> /dev/null
+
+echo -e "Cleaning..."
+rm *.bin *.img *.iso *.o *.vmdk 2> /dev/null
+
+echo -e "\nBuilding..."
+nasm ./boot/bootloader.asm -I ./boot/ -o ./bin/bootloader.bin || exit 1
+nasm -felf32 ./kernel/entry.asm -I ./kernel/ -o ./build/kernelEntryPoint.o || exit 1
+
+use_crystal_kernel=${USE_CRYSTAL_KERNEL:-true}
+
+if [ "$use_crystal_kernel" = true ]
+then
+	# Build crystal kernel without stdlib, using the custom prelude, and targeting i386-unknown-linux-elf
+	crystal build \
+		--no-debug \
+		--cross-compile \
+		--target i386-unknown-linux-elf \
+		-Dkernel \
+		--prelude=../prelude.cr \
+		-o ./build/kernel ./kernel/kernel.cr || exit 1
+
+	# Link the kernel into an ELF executable, then convert it to a flat binary for the bootloader.
+	ld \
+	  -m elf_i386 \
+	  -nostdlib \
+	  -T ./kernel/linker.ld \
+	  -o ./bin/kernel.elf ./build/kernelEntryPoint.o ./build/kernel.o || exit 1
+	
+	# Copy kernel.elf to kernel.bin properly.
+	objcopy \
+	  -O binary ./bin/kernel.elf ./bin/kernel.bin || exit 1
+else
+  # Build c++ kernel without stdlib, using the custom prelude, and targeting i386-unknown-linux-elf
+	clang++ -v \
+	  $CC_FLAGS \
+	  $LD_FLAGS \
+	  $CPP_FILES || exit 1
+fi
+
+cp ./bin/kernel.bin ./iso/boot/kernel.bin || exit 1
+
+./tools/genVDisk \
+  --output "crystalos" \
+  --floppy \
+	--bootloader ./bin/bootloader.bin \
+	--kernel ./bin/kernel.bin || exit 1
+
+# Run
+
+echo -e "\nRunning..."
+
+use_vnc=${USE_VNC:-false}
+
+if [ "$use_VNC" = true ]
+then
+  qemu-system-i386 -drive if=floppy,index=0,format=raw,file=crystalos.img || exit 1
+else
+  qemu-system-i386 -drive if=floppy,index=0,format=raw,file=crystalos.img -display curses || exit 1
+fi
+
+exit 0

new file
tools/README.md
@@ -0,0 +1,7 @@
+genVDisk is from: 
+- https://fancykillerpanda.github.io/OS-Tutorial/02_bootloader/genVDisk-utility/
+- https://github.com/FancyKillerPanda/PandaOS/tree/master/src/genVDisk
+
+can also run on Android/Termux but needs to be re-built from source (easy)
+
+its here because not packaged and should be taken care of somehow.