├── .gitignore ├── README.md ├── src ├── linker.ld ├── drivers │ ├── x86 │ │ └── x86.zig │ ├── vga │ │ ├── color.zig │ │ └── buffer.zig │ └── keyboard │ │ └── keyboard.zig ├── main.zig └── terminal │ ├── cursor.zig │ ├── terminal.zig │ └── cursor_test.zig ├── LICENSE └── docs ├── 01_introduction.md ├── 03_terminal_improvements.md └── 02_hello_world.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/**/* 2 | zig-out/**/* 3 | 4 | zig-os 5 | zig-os.o 6 | 7 | *.log 8 | **/*.log 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-os 2 | A simple OS written in Zig following Philipp Oppermann's posts ["Writing an OS in Rust"](https://os.phil-opp.com/) 3 | 4 | ## Tools 5 | 1. `zig 0.13.0` 6 | 2. `qemu` 7 | 8 | ## Summary 9 | 1. [Introduction](./docs/01_introduction.md) 10 | 2. [Hello World](./docs/02_hello_world.md) 11 | 3. [Terminal Improvements](./docs/03_terminal_improvements.md) 12 | -------------------------------------------------------------------------------- /src/linker.ld: -------------------------------------------------------------------------------- 1 | ENTRY(_start) 2 | 3 | SECTIONS { 4 | . = 1M; 5 | 6 | .multiboot : ALIGN(4K) { 7 | KEEP(*(.multiboot)) 8 | } 9 | 10 | .text : ALIGN(4K) { 11 | *(.text) 12 | } 13 | 14 | .rodata : ALIGN(4K) { 15 | *(.rodata) 16 | } 17 | 18 | .data : ALIGN(4K) { 19 | *(.data) 20 | } 21 | 22 | .bss : ALIGN(4K) { 23 | *(COMMON) 24 | *(.bss) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/drivers/x86/x86.zig: -------------------------------------------------------------------------------- 1 | pub inline fn inb(port: u16) u8 { 2 | return asm volatile ("inb %[port], %[result]" 3 | : [result] "={al}" (-> u8), // Output constraint 4 | : [port] "N{dx}" (port), // Input constraint 5 | ); 6 | } 7 | 8 | pub inline fn outb(port: u16, value: u8) void { 9 | asm volatile ("outb %[value], %[port]" 10 | : // No Outputs 11 | : [value] "{al}" (value), // Input constraints 12 | [port] "N{dx}" (port), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/drivers/vga/color.zig: -------------------------------------------------------------------------------- 1 | // Hardware text mode color constants 2 | const VgaColor = u8; 3 | pub const Color = enum(VgaColor) { 4 | Black = 0, 5 | Blue = 1, 6 | Green = 2, 7 | Cyan = 3, 8 | Red = 4, 9 | Magenta = 5, 10 | Brown = 6, 11 | LightGrey = 7, 12 | DarkGrey = 8, 13 | LightBlue = 9, 14 | LightGreen = 10, 15 | LightCyan = 11, 16 | LightRed = 12, 17 | LightMagenta = 13, 18 | LightBrown = 14, 19 | White = 15, 20 | }; 21 | 22 | // foreground | background color. 23 | pub fn vgaEntryColor(fg: Color, bg: Color) u8 { 24 | // will build the byte representing the color. 25 | return @intFromEnum(fg) | (@intFromEnum(bg) << 4); 26 | } 27 | 28 | pub fn vgaEntry(uc: u8, color: u8) u16 { 29 | const c: u16 = color; 30 | 31 | // build the 2 bytes representing the printable caracter w/ EntryColor. 32 | return uc | (c << 8); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rafael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const term = @import("terminal/terminal.zig").terminal; 4 | 5 | const MultiBoot = extern struct { 6 | magic: i32, 7 | flags: i32, 8 | checksum: i32, 9 | }; 10 | 11 | // 32 bits / 4 bytes integers 12 | const ALIGN = 1 << 0; // 0000 0000 0000 0000 0000 0000 0000 0001 13 | const MEMINFO = 1 << 1; // 0000 0000 0000 0000 0000 0000 0000 0010 14 | const FLAGS = ALIGN | MEMINFO; // 0000 0000 0000 0000 0000 0000 0000 0000 0011 15 | const MAGIC = 0x1BADB002; // 0001 1011 1010 1101 1011 0000 0000 0010 16 | 17 | // Define our stack size 18 | const STACK_SIZE = 16 * 1024; // 16 KB 19 | 20 | export var stack_bytes: [STACK_SIZE]u8 align(16) linksection(".bss") = undefined; 21 | 22 | export var multiboot align(4) linksection(".multiboot") = MultiBoot{ 23 | .magic = MAGIC, 24 | .flags = FLAGS, 25 | .checksum = -(MAGIC + FLAGS), 26 | }; 27 | 28 | export fn kernel_main() noreturn { 29 | term.initialize(); 30 | term.write("Hello, World!"); 31 | while (true) { 32 | term.handleInput(); 33 | } 34 | } 35 | 36 | export fn _start() callconv(.Naked) noreturn { 37 | // Set up the stack pointer 38 | asm volatile ( 39 | \\ mov $stack_bytes, %%esp 40 | \\ add %[stack_size], %%esp 41 | \\ call kernel_main 42 | : 43 | : [stack_size] "n" (STACK_SIZE), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/drivers/vga/buffer.zig: -------------------------------------------------------------------------------- 1 | pub const VGABuffer = struct { 2 | const Self = @This(); 3 | 4 | pub const HEIGHT = 25; 5 | pub const WIDTH = 80; 6 | const BUFFER_ADDRESS = 0xB8000; 7 | 8 | buffer: [*]volatile u16, 9 | 10 | var instance = init(); 11 | 12 | fn init() Self { 13 | return Self{ 14 | .buffer = @ptrFromInt(BUFFER_ADDRESS), 15 | }; 16 | } 17 | 18 | pub fn getInstance() *Self { 19 | return &instance; 20 | } 21 | 22 | pub fn flush(self: *Self, color: u8) void { 23 | var y: usize = 0; 24 | while (y < HEIGHT) : (y += 1) { 25 | var x: usize = 0; 26 | while (x < WIDTH) : (x += 1) { 27 | self.writeAt(' ', color, x, y); 28 | } 29 | } 30 | } 31 | 32 | pub fn writeAt(self: *Self, char: u8, color: u8, x: usize, y: usize) void { 33 | const index = y * WIDTH + x; 34 | self.buffer[index] = createEntry(char, color); 35 | } 36 | 37 | pub fn scroll(self: *Self, color: u8) void { 38 | var y: usize = 1; 39 | while (y < HEIGHT) : (y += 1) { 40 | var x: usize = 0; 41 | 42 | while (x < WIDTH) : (x += 1) { 43 | const from_index = y * WIDTH + x; 44 | const to_index = (y - 1) * WIDTH + x; 45 | self.buffer[to_index] = self.buffer[from_index]; 46 | } 47 | } 48 | 49 | var x: usize = 0; 50 | while (x < WIDTH) : (x += 1) { 51 | self.writeAt(' ', color, x, HEIGHT - 1); 52 | } 53 | } 54 | 55 | fn createEntry(char: u8, color: u8) u16 { 56 | const c: u16 = color; 57 | return char | (c << 8); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/terminal/cursor.zig: -------------------------------------------------------------------------------- 1 | pub const Cursor = struct { 2 | row: usize, 3 | column: usize, 4 | max_width: usize, 5 | max_height: usize, 6 | needs_scroll: bool, 7 | 8 | const Self = @This(); 9 | 10 | pub fn init(width: usize, height: usize) Self { 11 | return Self{ 12 | .row = 0, 13 | .max_height = height, 14 | 15 | .column = 0, 16 | .max_width = width, 17 | 18 | .needs_scroll = false, 19 | }; 20 | } 21 | 22 | pub fn getPosition(self: *const Self) struct { usize, usize } { 23 | return .{ self.row, self.column }; 24 | } 25 | 26 | pub fn moveTo(self: *Self, r: usize, col: usize) void { 27 | if (r >= self.max_height or col >= self.max_width) return; 28 | self.row = r; 29 | self.column = col; 30 | self.needs_scroll = false; 31 | } 32 | 33 | pub fn advance(self: *Self) void { 34 | self.column += 1; 35 | if (self.column >= self.max_width) { 36 | self.column = 0; 37 | self.row += 1; 38 | if (self.row >= self.max_height) { 39 | self.row = self.max_height - 1; 40 | self.needs_scroll = true; 41 | } 42 | } 43 | } 44 | 45 | pub fn backOne(self: *Self) void { 46 | if (self.column > 0) { 47 | self.column = self.column - 1; 48 | return; 49 | } 50 | if (self.row == 0) { 51 | return; 52 | } 53 | 54 | self.row = self.row - 1; 55 | self.column = self.max_width - 1; 56 | self.needs_scroll = false; 57 | } 58 | 59 | pub fn newLine(self: *Self) void { 60 | self.column = 0; 61 | self.row += 1; 62 | if (self.row >= self.max_height) { 63 | self.row -= 1; 64 | self.needs_scroll = true; 65 | } 66 | } 67 | 68 | pub fn reset(self: *Self) void { 69 | self.column = 0; 70 | self.row = 0; 71 | self.needs_scroll = false; 72 | } 73 | 74 | pub fn checkScroll(self: *Self) bool { 75 | const needs = self.needs_scroll; 76 | self.needs_scroll = false; 77 | return needs; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /docs/01_introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Summary 4 | 1. [About the Project](#about-the-project) 5 | - [Q&A](#q&a) 6 | - [Pre-requisites](#pre-requisites) 7 | 2. [Setup](#setup) 8 | - [Tools](#tools) 9 | - [Create Zig Build](#create-zig-build) 10 | 11 | ## About the Project 12 | 13 | ### Q&A 14 | 1. What's this repository based on? 15 | - > I bases this repository on Philipp Oppermann's posts ["Writing an OS in Rust"](https://os.phil-opp.com/), he wrote it using Rust, so, because I'm studying Zig I thought about writing the same in Zig. :D 16 | 2. Why? 17 | - > I want to learn. 18 | 3. Why zig? 19 | - > ... I want to learn ... zig 20 | 4. Which OS target? 21 | - I'll try to develop it to x86_64 architecture. 22 | 23 | 27 | 28 | ### Pre-requisites 29 | 1. No std: 30 | - > [...] we can’t use threads, files, heap memory, the network, random numbers, standard output, or any other features requiring OS abstractions or specific hardware. [...] 31 | 2. No Dependencies 32 | - > [...] create an executable that can be run without an underlying operating system. [...] 33 | 3. Use what's available: 34 | - > TODO: What can be used? 35 | 4. Install `qemu-system-x86` 36 | - > We'll be able to use `qemu-system-x86_64` to run our Minimal Kernel/OS. 37 | 38 | ## Setup 39 | This section will show you how to setup your environment to start working on the step-by-step guide. 40 | 41 | ### Tools 42 | - [Zig _(0.13)_](https://ziglang.org/) 43 | - [qemu-system](https://www.qemu.org/) 44 | - We'll be using the `qemu-system-x86` 45 | 46 | ### Create Zig Build 47 | - > $ zig init-exe 48 | This will generate the following structure: 49 | ```shell 50 | . 51 | ├── build.zig 52 | └── src 53 | └── main.zig 54 | ``` 55 | 56 | - `build.zig` 57 | - This file is responsible for defining how to build your program 58 | - You can define things like: 59 | - Target: Architecture, Operational System, etc. 60 | - Optimization: Release, Debug, etc. 61 | - `src/main.zig` 62 | - This is the program itself that will be compiled 63 | 64 | ### Final 65 | So, yeah, now we should have everything to start working on our minimal kernel! 66 | 67 | [Chapter 2 - Hello World](./02_hello_world.md) 68 | -------------------------------------------------------------------------------- /src/terminal/terminal.zig: -------------------------------------------------------------------------------- 1 | const vga = @import("../drivers/vga/color.zig"); 2 | const Color = vga.Color; 3 | const vga_buffer = @import("../drivers/vga/buffer.zig"); 4 | const VGABuffer = vga_buffer.VGABuffer; 5 | const Cursor = @import("cursor.zig").Cursor; 6 | const Keyboard = @import("../drivers/keyboard/keyboard.zig").Keyboard; 7 | 8 | pub const terminal = struct { 9 | var color = vga.vgaEntryColor(Color.LightGrey, Color.Black); 10 | var cursor = Cursor.init(VGABuffer.WIDTH, VGABuffer.HEIGHT); 11 | const buffer: *VGABuffer = VGABuffer.getInstance(); 12 | 13 | const TAB_SIZE = 4; 14 | 15 | pub fn initialize() void { 16 | buffer.flush(color); 17 | Keyboard.initialize(); 18 | } 19 | 20 | fn putChar(c: u8, new_color: u8) void { 21 | switch (c) { 22 | '\n' => { 23 | cursor.newLine(); 24 | if (cursor.checkScroll()) { 25 | buffer.scroll(color); 26 | } 27 | }, 28 | '\t' => { 29 | const spaces = TAB_SIZE - (cursor.column % TAB_SIZE); 30 | var i: usize = 0; 31 | while (i < spaces) : (i += 1) { 32 | buffer.writeAt(' ', new_color, cursor.column, cursor.row); 33 | cursor.advance(); 34 | } 35 | }, 36 | '\r' => cursor.column = 0, 37 | 0x08 => { 38 | cursor.backOne(); 39 | buffer.writeAt(' ', color, cursor.column, cursor.row); 40 | }, 41 | else => { 42 | buffer.writeAt(c, new_color, cursor.column, cursor.row); 43 | cursor.advance(); 44 | }, 45 | } 46 | } 47 | 48 | pub fn write(data: []const u8) void { 49 | for (data) |c| 50 | putChar(c, color); 51 | } 52 | 53 | pub fn handleInput() void { 54 | const scancode = Keyboard.readScancode(); 55 | if (Keyboard.handleScancode(scancode)) |char| { 56 | switch (char) { 57 | 0x08 => { // backspace 58 | cursor.backOne(); 59 | buffer.writeAt(' ', color, cursor.column, cursor.row); 60 | }, 61 | 0x09 => { // tab 62 | const spaces = TAB_SIZE - (cursor.column % TAB_SIZE); 63 | var i: usize = 0; 64 | while (i < spaces) : (i += 1) { 65 | putChar(' ', color); 66 | } 67 | }, 68 | 0x0A => { // enter 69 | cursor.newLine(); 70 | if (cursor.checkScroll()) { 71 | buffer.scroll(color); 72 | } 73 | }, 74 | else => { 75 | putChar(char, color); 76 | }, 77 | } 78 | } 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/drivers/keyboard/keyboard.zig: -------------------------------------------------------------------------------- 1 | const x86 = @import("../x86/x86.zig"); 2 | 3 | pub const Keyboard = struct { 4 | // Keyboard I/O ports 5 | const DATA_PORT: u16 = 0x60; 6 | const STATUS_PORT: u16 = 0x64; 7 | const COMMAND_PORT: u16 = 0x64; 8 | 9 | // Special keycodes 10 | const BACKSPACE: u8 = 0x0E; 11 | const TAB: u8 = 0x0F; 12 | const ENTER: u8 = 0x1C; 13 | const LEFT_SHIFT: u8 = 0x2A; 14 | const RIGHT_SHIFT: u8 = 0x36; 15 | const CAPS_LOCK: u8 = 0x3A; 16 | 17 | // State tracking 18 | var shift_pressed: bool = false; 19 | var caps_lock: bool = false; 20 | 21 | // Basic US keyboard layout mapping 22 | const ascii_table = [_]u8{ 0, 0, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 0, 0, 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 0, 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', 0, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0, '*', 0, ' ' }; 23 | 24 | // Shift key mapping for numbers and symbols 25 | const shift_table = [_]u8{ 0, 0, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~', 0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, '*', 0, ' ' }; 26 | 27 | pub fn initialize() void { 28 | // TODO: Set up keyboard interrupt handler (IRQ 1) 29 | // This will be implemented when we set up the IDT 30 | } 31 | 32 | pub fn handleScancode(scancode: u8) ?u8 { 33 | // Key release codes start at 0x80 34 | const released = (scancode & 0x80) != 0; 35 | const code = scancode & 0x7F; 36 | 37 | if (released) { 38 | switch (code) { 39 | LEFT_SHIFT, RIGHT_SHIFT => shift_pressed = false, 40 | else => {}, 41 | } 42 | return null; 43 | } 44 | 45 | // Handle key press 46 | switch (code) { 47 | LEFT_SHIFT, RIGHT_SHIFT => { 48 | shift_pressed = true; 49 | return null; 50 | }, 51 | CAPS_LOCK => { 52 | caps_lock = !caps_lock; 53 | return null; 54 | }, 55 | BACKSPACE => return 0x08, // ASCII backspace 56 | ENTER => return 0x0A, // ASCII newline 57 | TAB => return 0x09, 58 | else => { 59 | if (code >= ascii_table.len) return null; 60 | 61 | var char = ascii_table[code]; 62 | if (char == 0) return null; 63 | 64 | // Apply shift and caps lock modifications 65 | const should_shift = shift_pressed != caps_lock; 66 | if (should_shift) { 67 | if (code < shift_table.len) { 68 | char = shift_table[code]; 69 | } 70 | } 71 | 72 | return char; 73 | }, 74 | } 75 | } 76 | 77 | pub fn readScancode() u8 { 78 | // Wait for keyboard buffer to be ready 79 | while ((x86.inb(STATUS_PORT) & 1) == 0) {} 80 | return x86.inb(DATA_PORT); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/terminal/cursor_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const Cursor = @import("cursor.zig").Cursor; 4 | 5 | test "Cursor - initialization" { 6 | const cursor = Cursor.init(80, 25); 7 | 8 | try testing.expectEqual(@as(usize, 0), cursor.row); 9 | try testing.expectEqual(@as(usize, 0), cursor.column); 10 | try testing.expectEqual(@as(usize, 25), cursor.max_height); 11 | try testing.expectEqual(@as(usize, 80), cursor.max_width); 12 | try testing.expectEqual(false, cursor.needs_scroll); 13 | } 14 | 15 | test "Cursor - getPosition" { 16 | var cursor = Cursor.init(80, 25); 17 | cursor.row = 5; 18 | cursor.column = 10; 19 | 20 | const pos = cursor.getPosition(); 21 | try testing.expectEqual(@as(usize, 5), pos[0]); 22 | try testing.expectEqual(@as(usize, 10), pos[1]); 23 | } 24 | 25 | test "Cursor - moveTo valid position" { 26 | var cursor = Cursor.init(80, 25); 27 | cursor.moveTo(5, 10); 28 | 29 | try testing.expectEqual(@as(usize, 5), cursor.row); 30 | try testing.expectEqual(@as(usize, 10), cursor.column); 31 | try testing.expectEqual(false, cursor.needs_scroll); 32 | } 33 | 34 | test "Cursor - moveTo invalid position" { 35 | var cursor = Cursor.init(80, 25); 36 | const initial_row = cursor.row; 37 | const initial_col = cursor.column; 38 | 39 | // Try to move beyond bounds 40 | cursor.moveTo(100, 90); 41 | 42 | // Should remain at initial position 43 | try testing.expectEqual(initial_row, cursor.row); 44 | try testing.expectEqual(initial_col, cursor.column); 45 | } 46 | 47 | test "Cursor - advance within bounds" { 48 | var cursor = Cursor.init(80, 25); 49 | cursor.moveTo(0, 78); 50 | cursor.advance(); 51 | 52 | try testing.expectEqual(@as(usize, 0), cursor.row); 53 | try testing.expectEqual(@as(usize, 79), cursor.column); 54 | try testing.expectEqual(false, cursor.needs_scroll); 55 | } 56 | 57 | test "Cursor - advance with line wrap" { 58 | var cursor = Cursor.init(80, 25); 59 | cursor.moveTo(0, 79); 60 | cursor.advance(); 61 | 62 | try testing.expectEqual(@as(usize, 1), cursor.row); 63 | try testing.expectEqual(@as(usize, 0), cursor.column); 64 | try testing.expectEqual(false, cursor.needs_scroll); 65 | } 66 | 67 | test "Cursor - advance with scroll needed" { 68 | var cursor = Cursor.init(80, 25); 69 | cursor.moveTo(24, 79); 70 | cursor.advance(); 71 | 72 | try testing.expectEqual(@as(usize, 24), cursor.row); 73 | try testing.expectEqual(@as(usize, 0), cursor.column); 74 | try testing.expectEqual(true, cursor.needs_scroll); 75 | } 76 | 77 | test "Cursor - backOne within line" { 78 | var cursor = Cursor.init(80, 25); 79 | cursor.moveTo(5, 10); 80 | cursor.backOne(); 81 | 82 | try testing.expectEqual(@as(usize, 5), cursor.row); 83 | try testing.expectEqual(@as(usize, 9), cursor.column); 84 | try testing.expectEqual(false, cursor.needs_scroll); 85 | } 86 | 87 | test "Cursor - backOne with line wrap" { 88 | var cursor = Cursor.init(80, 25); 89 | cursor.moveTo(5, 0); 90 | cursor.backOne(); 91 | 92 | try testing.expectEqual(@as(usize, 4), cursor.row); 93 | try testing.expectEqual(@as(usize, 79), cursor.column); 94 | try testing.expectEqual(false, cursor.needs_scroll); 95 | } 96 | 97 | test "Cursor - backOne at start" { 98 | var cursor = Cursor.init(80, 25); 99 | cursor.backOne(); 100 | 101 | try testing.expectEqual(@as(usize, 0), cursor.row); 102 | try testing.expectEqual(@as(usize, 0), cursor.column); 103 | try testing.expectEqual(false, cursor.needs_scroll); 104 | } 105 | 106 | test "Cursor - newLine within bounds" { 107 | var cursor = Cursor.init(80, 25); 108 | cursor.moveTo(5, 10); 109 | cursor.newLine(); 110 | 111 | try testing.expectEqual(@as(usize, 6), cursor.row); 112 | try testing.expectEqual(@as(usize, 0), cursor.column); 113 | try testing.expectEqual(false, cursor.needs_scroll); 114 | } 115 | 116 | test "Cursor - newLine with scroll needed" { 117 | var cursor = Cursor.init(80, 25); 118 | cursor.moveTo(24, 10); 119 | cursor.newLine(); 120 | 121 | try testing.expectEqual(@as(usize, 24), cursor.row); 122 | try testing.expectEqual(@as(usize, 0), cursor.column); 123 | try testing.expectEqual(true, cursor.needs_scroll); 124 | } 125 | 126 | test "Cursor - reset" { 127 | var cursor = Cursor.init(80, 25); 128 | cursor.moveTo(5, 10); 129 | cursor.needs_scroll = true; 130 | cursor.reset(); 131 | 132 | try testing.expectEqual(@as(usize, 0), cursor.row); 133 | try testing.expectEqual(@as(usize, 0), cursor.column); 134 | try testing.expectEqual(false, cursor.needs_scroll); 135 | } 136 | 137 | test "Cursor - checkScroll" { 138 | var cursor = Cursor.init(80, 25); 139 | cursor.needs_scroll = true; 140 | 141 | try testing.expectEqual(true, cursor.checkScroll()); 142 | try testing.expectEqual(false, cursor.needs_scroll); 143 | try testing.expectEqual(false, cursor.checkScroll()); 144 | } 145 | -------------------------------------------------------------------------------- /docs/03_terminal_improvements.md: -------------------------------------------------------------------------------- 1 | # Terminal Improvements 2 | 3 | If you didn't read the [Second Chapter(Hello World)](./02_hello_world.md), please read it first. 4 | 5 | ## Summary 6 | 1. [Current State](#current-state) 7 | 8 | ## Current State 9 | 10 | Looking at the `src/terminal/terminal.zig` implementation, it combines several distinct responsibilities: 11 | 1. VGA color management 12 | 2. Terminal buffer management 13 | 3. Character output handling 14 | 4. Screen positioning logic 15 | 16 | ### VGA Color Management 17 | 18 | First, let's create a new file at `src/drivers/vga/color.zig`: 19 | ```zig 20 | // Hardware text mode color constants 21 | const VgaColor = u8; 22 | pub const Color = enum(VgaColor) { 23 | Black = 0, 24 | Blue = 1, 25 | Green = 2, 26 | Cyan = 3, 27 | Red = 4, 28 | Magenta = 5, 29 | Brown = 6, 30 | LightGrey = 7, 31 | DarkGrey = 8, 32 | LightBlue = 9, 33 | LightGreen = 10, 34 | LightCyan = 11, 35 | LightRed = 12, 36 | LightMagenta = 13, 37 | LightBrown = 14, 38 | White = 15, 39 | }; 40 | 41 | // foreground | background color. 42 | pub fn vgaEntryColor(fg: Color, bg: Color) u8 { 43 | // will build the byte representing the color. 44 | return @intFromEnum(fg) | (@intFromEnum(bg) << 4); 45 | } 46 | 47 | pub fn vgaEntry(uc: u8, color: u8) u16 { 48 | const c: u16 = color; 49 | 50 | // build the 2 bytes representing the printable caracter w/ EntryColor. 51 | return uc | (c << 8); 52 | } 53 | ``` 54 | 55 | Main difference here is the use of Enums just for strictly typing. 56 | 57 | Your `src/terminal/terminal.zig` should look like this: 58 | ```zig 59 | // we can import our driver here 60 | const vga = @import("../drivers/vga/color.zig"); 61 | const Color = vga.Color; // setting Color as const. 62 | 63 | const VGA_WIDTH = 80; 64 | const VGA_HEIGHT = 45; 65 | const VGA_BUFFER_ADDRESS = 0xB8000; 66 | 67 | pub const terminal = struct { 68 | var row: usize = 0; 69 | var column: usize = 0; 70 | 71 | var color = vga.vgaEntryColor(Color.LightGrey, Color.Black); 72 | 73 | const buffer: [*]volatile u16 = @ptrFromInt(VGA_BUFFER_ADDRESS); 74 | 75 | pub fn initialize() void { 76 | var y: usize = 0; 77 | while (y < VGA_HEIGHT) : (y += 1) { 78 | var x: usize = 0; 79 | while (x < VGA_WIDTH) : (x += 1) { 80 | putCharAt(' ', color, x, y); 81 | } 82 | } 83 | } 84 | 85 | fn setColor(new_color: u8) void { 86 | color = new_color; 87 | } 88 | 89 | fn putCharAt(c: u8, new_color: u8, x: usize, y: usize) void { 90 | const index = y * VGA_WIDTH + x; 91 | buffer[index] = vga.vgaEntry(c, new_color); 92 | } 93 | 94 | fn putChar(c: u8) void { 95 | putCharAt(c, color, column, row); 96 | column += 1; 97 | if (column == VGA_WIDTH) { 98 | column = 0; 99 | row += 1; 100 | if (row == VGA_HEIGHT) 101 | row = 0; 102 | } 103 | } 104 | 105 | pub fn write(data: []const u8) void { 106 | for (data) |c| 107 | putChar(c); 108 | } 109 | }; 110 | ``` 111 | 112 | Looks cleaner, but, what about the `buffer`? In this case it makes more sense to live in the vga package, so, in the next we'll be implementing that. 113 | 114 | ### Terminal Buffer Management 115 | 116 | First, let's create a new file at `src/drivers/vga/buffer.zig`: 117 | ```zig 118 | pub const VGABuffer = struct { 119 | const Self = @This(); 120 | 121 | pub const WIDTH = 80; 122 | pub const HEIGHT = 45; 123 | const BUFFER_ADDRESS = 0xB8000; 124 | 125 | buffer: [*]volatile u16, 126 | 127 | var instance = init(); 128 | 129 | fn init() Self { 130 | return Self{ 131 | .buffer = @ptrFromInt(BUFFER_ADDRESS), 132 | }; 133 | } 134 | 135 | pub fn getInstance() *Self { 136 | return &instance; 137 | } 138 | 139 | pub fn writeAt(self: *Self, char: u8, color: u8, x: usize, y: usize) void { 140 | const index = y * WIDTH + x; 141 | self.buffer[index] = createEntry(char, color); 142 | } 143 | 144 | fn createEntry(char: u8, color: u8) u16 { 145 | const c: u16 = color; 146 | return char | (c << 8); 147 | } 148 | }; 149 | ``` 150 | 151 | Now implementing it back in `src/terminal/terminal.zig` should look like this: 152 | ```zig 153 | const vga = @import("../drivers/vga/color.zig"); 154 | const Color = vga.Color; 155 | const vga_buffer = @import("../drivers/vga/buffer.zig"); 156 | const VGABuffer = vga_buffer.VGABuffer; 157 | 158 | pub const terminal = struct { 159 | var row: usize = 0; 160 | var column: usize = 0; 161 | var color = vga.vgaEntryColor(Color.LightGrey, Color.Black); 162 | const buffer: *VGABuffer = VGABuffer.getInstance(); 163 | 164 | pub fn initialize() void { 165 | var y: usize = 0; 166 | while (y < VGABuffer.HEIGHT) : (y += 1) { 167 | var x: usize = 0; 168 | while (x < VGABuffer.WIDTH) : (x += 1) { 169 | putCharAt(' ', color, x, y); 170 | } 171 | } 172 | } 173 | 174 | fn putCharAt(c: u8, new_color: u8, x: usize, y: usize) void { 175 | buffer.writeAt(c, new_color, x, y); 176 | } 177 | 178 | fn putChar(c: u8) void { 179 | putCharAt(c, color, column, row); 180 | column += 1; 181 | if (column == VGABuffer.WIDTH) { 182 | column = 0; 183 | row += 1; 184 | if (row == VGABuffer.HEIGHT) 185 | row = 0; 186 | } 187 | } 188 | 189 | pub fn write(data: []const u8) void { 190 | for (data) |c| 191 | putChar(c); 192 | } 193 | }; 194 | ``` 195 | 196 | This code still has a thing that it doesn't make too much sense having it inside `Terminal` struct, and that's the position logic. 197 | 198 | ### Cursor positioning logic 199 | 200 | First, let's create a new file at `src/terminal/cursor.zig`: 201 | ```zig 202 | pub const Cursor = struct { 203 | row: usize, 204 | column: usize, 205 | max_width: usize, 206 | max_height: usize, 207 | 208 | const Self = @This(); 209 | 210 | pub fn init(width: usize, height: usize) Self { 211 | return Self{ 212 | .row = 0, 213 | .column = 0, 214 | .max_width = width, 215 | .max_height = height, 216 | }; 217 | } 218 | 219 | pub fn getPosition(self: *const Self) struct { usize, usize } { 220 | return .{ self.row, self.column }; 221 | } 222 | 223 | pub fn moveTo(self: *Self, r: usize, col: usize) void { 224 | if (r >= self.max_height or col >= self.max_width) return; 225 | self.row = r; 226 | self.column = col; 227 | } 228 | 229 | pub fn advance(self: *Self) void { 230 | self.column += 1; 231 | if (self.column >= self.max_width) { 232 | self.column = 0; 233 | self.row += 1; 234 | if (self.row >= self.max_height) { 235 | self.row = 0; 236 | } 237 | } 238 | } 239 | 240 | pub fn newLine(self: *Self) void { 241 | self.column = 0; 242 | self.row += 1; 243 | if (self.row >= self.max_height) { 244 | self.row = 0; 245 | } 246 | } 247 | 248 | pub fn reset(self: *Self) void { 249 | self.column = 0; 250 | self.row = 0; 251 | } 252 | }; 253 | ``` 254 | 255 | Also added some helper functions that we may use in the future! 256 | 257 | Your `src/terminal/terminal.zig` code should look like this: 258 | ```zig 259 | const vga = @import("../drivers/vga/color.zig"); 260 | const Color = vga.Color; 261 | const vga_buffer = @import("../drivers/vga/buffer.zig"); 262 | const VGABuffer = vga_buffer.VGABuffer; 263 | const Cursor = @import("cursor.zig").Cursor; 264 | 265 | pub const terminal = struct { 266 | var color = vga.vgaEntryColor(Color.LightGrey, Color.Black); 267 | var cursor = Cursor.init(VGABuffer.HEIGHT, VGABuffer.WIDTH); 268 | const buffer: *VGABuffer = VGABuffer.getInstance(); 269 | 270 | pub fn initialize() void { 271 | buffer.flush(color); 272 | } 273 | 274 | fn putChar(c: u8, new_color: u8) void { 275 | buffer.writeAt(c, new_color, cursor.column, cursor.row); 276 | cursor.advance(); 277 | } 278 | 279 | pub fn write(data: []const u8) void { 280 | for (data) |c| 281 | putChar(c, color); 282 | } 283 | }; 284 | ``` 285 | -------------------------------------------------------------------------------- /docs/02_hello_world.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | If you didn't read the [First Chapter(Introduction)](./01_introduction.md), please read it first. 3 | 4 | ## Summary 5 | 1. [Introduction](#introduction) 6 | - [Running as it is](#running-as-it-is) 7 | 2. [PHV ELF Note](#phv-elf-note) 8 | - [PHV](#phv) 9 | - [ELF Note](#elf-note) 10 | 3. [Target](#target) 11 | - [Setting x86-freestanding Target](#setting-x86-freestanding-target) 12 | - [Building with Custom Target](#building-with-custom-target) 13 | 4. [Linker](#linker) 14 | - [Linker File](#linker-file) 15 | - [Using LD File](#using-ld-file) 16 | - [Building with Linker](#building-with-linker) 17 | 5. [Main File](#main-file) 18 | - [Multiboot](#multiboot) 19 | - [Consts](#consts) 20 | - [Linking Multiboot](#Linking Multiboot) 21 | - [Entry Function](#entry-function) 22 | - [Defining Start](#defining-start) 23 | 6. [Printing Hello](#printing-hello) 24 | - [Colors](#colors) 25 | - [Dimensions](#dimensions) 26 | - [Terminal Struct](#terminal-struct) 27 | - [Attributes](#attributes) 28 | - [Functions](#functions) 29 | - [Implementing Terminal](#implementing-terminal) 30 | - [Printing](#printing) 31 | - [Next Steps](#next-steps) 32 | 33 | ## Introduction 34 | In this Chapter, we'll be trying to show a simple "Hello, World" running on a `x86-freestanding` architecture. 35 | 36 | ### Running as it is 37 | Let's try to run as it is right now: 38 | 39 | Building: 40 | - > $ zig build 41 | - This will compile our code into a binary. 42 | 43 | This will generate: 44 | ```shell 45 | . 46 | ├── build.zig 47 | ├── src 48 | │   └── main.zig 49 | ├── zig-cache 50 | │   ├── h 51 | │   │   └── ... 52 | │   ├── o 53 | │   │   └── ... 54 | │   ├── tmp 55 | │   └── z 56 | │   └── ... 57 | └── zig-out 58 | └── bin 59 | └── zig-os 60 | ``` 61 | The `zig-out/bin/zig-os` is what we need. 62 | 63 | Running `qemu`: 64 | - > $ qemu-system-x86_64 -kernel zig-out/bin/zig-os 65 | - This will run a `x86_64` "machine" with our binary as a _kernel_ 66 | 67 | Output: 68 | ```shell 69 | qemu-system-x86_64: Error loading uncompressed kernel without PVH ELF Note 70 | ``` 71 | 72 | It didn't work :( , let's try to understand it in the next section -> 73 | 74 | ## PHV ELF Note 75 | What does this mean: 76 | - _"qemu-system-x86_64: Error loading uncompressed kernel without PVH ELF Note"_ ? 77 | 78 | ### PHV 79 | - Is a mix between HVM and PV: 80 | - HVM: _"HVM (known as Hardware Virtual Machine) is the type of instance that mimics bare-metal server setup which provides better hardware isolation. With this instance type, the OS can run directly on top of the Virtual Machine without additional configuration making it to look like it is running on a real physical server. For more information about this instance type and the other one that is most commonly used, [...]"_ 81 | - PV: _"Paravirtualization (PV) is an efficient and lightweight virtualization technique introduced by the Xen Project team [...] PV does not require virtualization extensions from the host CPU and thus enables virtualization on hardware architectures that do not support Hardware-assisted virtualization. [...]"_ 82 | 83 | ### ELF Note 84 | E.L.F. = "Executable and Linkable Format" 85 | _"[...] is a common standard file format for executable files, object code, shared libraries, and core dumps. [...]"_ 86 | 87 | ### Resolution 88 | So, basically the binary that we're trying to run as a kernel is missing the `PVH ELF Note`, to solve that: 89 | 90 | ## Target 91 | Basically what is happening when running `zig build` is building a binary matching your machine architecture, let's change this: 92 | 93 | In `build.zig`, we currently have: 94 | ```zig 95 | const std = @import("std"); 96 | 97 | // ... 98 | pub fn build(b: *std.Build) void { 99 | // ... 100 | const target = b.standardTargetOptions(.{}); 101 | 102 | /// ... 103 | } 104 | ``` 105 | 106 | #### Setting x86-freestanding Target 107 | Let's add the following imports after `const std = ...`: 108 | ```zig 109 | const std = @import("std"); 110 | const Target = std.Target; 111 | const CrossTarget = std.zig.CrossTarget; 112 | ``` 113 | - `Target` 114 | - It stores the `enum` for different CPU architectures and OS tags 115 | - `CrossTarget` 116 | - This struct will set our `const target` value/type. 117 | 118 | 119 | Then, let's declare our target as: 120 | ```zig 121 | pub fn build(b: *std.Build) void { 122 | // Currently the target is hardcoded to be 123 | // on x86, but with time this will be compiled for 124 | // various architectures. 125 | const targetQuery = CrossTarget{ 126 | .cpu_arch = Target.Cpu.Arch.x86, 127 | .os_tag = Target.Os.Tag.freestanding, 128 | }; 129 | 130 | const target = b.resolveTargetQuery(targetQuery); 131 | 132 | // ... 133 | } 134 | ``` 135 | Our CPU architecture will be `x86` and the OS tag `freestanding`, meaning: We want to build a binary for `CPU x86` without and underlying OS. 136 | 137 | Let's try to build and run it again: 138 | 139 | #### Building with Custom Target 140 | - > $ zig build 141 | - > $ qemu-system-x86_64 -kernel zig-out/bin/zig-os 142 | Output: 143 | ```shell 144 | qemu-system-x86_64: Error loading uncompressed kernel without PVH ELF Note 145 | ``` 146 | Hmmm, we got the same error, again. Maybe it's a linker issue? 147 | 148 | ## Linker 149 | _"[...] An LD file is a script written in the GNU "linker command language." It contains one or more commands that are used to configure how input files storing static object code are to be compiled into a single executable program or library for the GNU operating system. [...]"_ 150 | 151 | 152 | Let's add a `src/linker.ld` file: 153 | ```ld 154 | ENTRY(_start) 155 | 156 | SECTIONS { 157 | . = 1M; 158 | 159 | .multiboot : ALIGN(4K) { 160 | KEEP(*(.multiboot)) 161 | } 162 | 163 | .text : ALIGN(4K) { 164 | *(.text) 165 | } 166 | 167 | .rodata : ALIGN(4K) { 168 | *(.rodata) 169 | } 170 | 171 | .data : ALIGN(4K) { 172 | *(.data) 173 | } 174 | 175 | .bss : ALIGN(4K) { 176 | *(COMMON) 177 | *(.bss) 178 | } 179 | } 180 | ``` 181 | Well, that's a lot of things that I've never seen before, let's try to understand what is happening. 182 | 183 | ### Linker File 184 | - `01: ENTRY(_start)` 185 | - [ENTRY](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC24) 186 | - _"[...] command specifically for defining the first executable instruction in an output file [...]"_ 187 | - So this, mean that we'll be specifying which section of our code will be the entrypoint when running our binary. 188 | - In this case, we defined as `_start` 189 | - `03: SECTIONS {` 190 | - [SECTIONS](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC17) 191 | - _"[...] `SECTIONS` command controls exactly where input sections are placed into output sections, their order in the output file, and to which output sections they are allocated. [...]"_ 192 | - So basically with `SECTIONS` you can do one of: 193 | - define the entry point 194 | - assign a value to a symbol 195 | - describe the placement of a named output section, and which input sections go into it. 196 | - `04: . = 1M;` 197 | - [Location Counter](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC10) 198 | - _"The special linker variable dot `.` always contains the current output location counter. [...]"_ 199 | - This will reserve a 1M storage section in our _kernel_ 200 | - TODO: CHECK THIS - I'm not sure about it. 201 | - `06: .text : ALIGN(4K) {` 202 | - `.text` is the name of the _section_ 203 | - `: ALIGN(4K) {` 204 | - [Arithmetic Functions](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC14) 205 | - _"`ALIGN(exp)` - Return the result of the current location counter `(.)` aligned to the next `exp` boundary. [...]"_ 206 | - `07: KEEP(*(.multiboot))` 207 | - [KEEP](https://www.microchip.com/forums/m384725.aspx) 208 | - This marks a section that SHOULDN'T be eliminated 209 | - `*(.multiboot)` 210 | - `*(exp)` - _"You can name one or more sections from your input files, for insertion in the current output section. [...]"_ 211 | TODO: FINISH THIS SECTION 212 | 213 | ### Using LD File 214 | To use our `src/linker.ld` file we should modify our `build.zig`, loading the `src/linker.ld` using: 215 | - `exe.setLinkerScriptPath(b.path("src/linker.ld"));` 216 | 217 | ```zig 218 | const exe = b.addExecutable(.{ 219 | .name = "zig-os", 220 | // In this case the main source file is merely a path, however, in more 221 | // complicated build scripts, this could be a generated file. 222 | .root_source_file = b.path("src/main.zig"), 223 | .target = target, 224 | .optimize = optimize, 225 | }); 226 | 227 | exe.setLinkerScriptPath(b.path("src/linker.ld")); 228 | ``` 229 | 230 | ### Building with Linker 231 | Let's try again building it. 232 | 1. > $ zig build 233 | 2. > $ qemu-system-x86_64 -kernel zig-out/bin/zig-os 234 | ```shell 235 | qemu-system-x86_64: Error loading uncompressed kernel without PVH ELF Note 236 | ``` 237 | 238 | Still, the same error, if we look at the first line of our `linker.ld` file, you can see that the entrypoint of our program should be `_start`, but we don't have it, so, in the next section we'll be modifying our `src/main.zig` and finally be able to "boot" our kernel. 239 | 240 | ## Main File 241 | First of all, clean the `src/main.zig` and let's add something there. 242 | 243 | ### Multiboot 244 | We need to define a `MultiBoot` struct following the pattern from [Header Magic Fields](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Header-magic-fields) config, we need 3 fields 245 | - `magic: i32` 246 | - _"The field `magic` is the magic number identifying the header, which **must** be the hexadecimal value `0x1BADB002`."_ 247 | - `flags: i32` 248 | - _"specifies features that the OS image requests or requires of an boot loader."_ 249 | - `checksum: i32` 250 | - _"is a 32-bit unsigned value which, when added to the other magic fields (i.e. `magic` and `flags`), must have a 32-bit unsigned sum of zero."_ 251 | [Multiboot Manual](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html) 252 | 253 | #### Struct 254 | With that in mind we should implement our struct: 255 | ```zig 256 | const MultiBoot = extern struct { 257 | magic: i32, 258 | flags: i32, 259 | checksum: i32, 260 | }; 261 | ``` 262 | 263 | #### Consts: 264 | Following the information from [Header Magic Fields](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Header-magic-fields): 265 | ```const 266 | const ALIGN = 1 << 0; // 0000 0000 0000 0000 0000 0000 0000 0001 267 | const MEMINFO = 1 << 1; // 0000 0000 0000 0000 0000 0000 0000 0010 268 | const FLAGS = ALIGN | MEMINFO; // 0000 0000 0000 0000 0000 0000 0000 0000 0011 269 | const MAGIC = 0x1BADB002; // 0001 1011 1010 1101 1011 0000 0000 0010 270 | ``` 271 | 272 | Being: 273 | - `ALIGN`: 274 | - TODO: 275 | - `MEMINFO`: 276 | - TODO: 277 | - `FLAGS`: 278 | - TODO: 279 | - `MAGIC`: 280 | - The default hexadecimal value: `0x1BADB002` 281 | 282 | #### Linking Multiboot 283 | Now we should link our `MultiBoot struct` with our `src/linker.ld`'s `.multiboot` section: 284 | ```zig 285 | export var multiboot align(4) linksection(".multiboot") = MultiBoot{ 286 | .magic = MAGIC, 287 | .flags = FLAGS, 288 | .checksum = -(MAGIC + FLAGS), 289 | }; 290 | ``` 291 | - `export var multiboot align(4) linksection(".multiboot")`: 292 | - `export`: _"[...] makes a function or variable externally visible in the generated object file. [...]"_ 293 | - `var`: _"declares a variable that may be modified."_ 294 | - `multiboot`: The name of the variable. 295 | - `align(4)`: 296 | - _"can be used to specify the alignment of a pointer. It can also be used after a variable or function declaration to specify the alignment of pointers to that variable or function."_ 297 | - Basically we're making this variable divisible by `4`, so _"when when a value of the type is loaded from or stored to memory, the memory address must be evenly divisible by this number."_. 298 | - `linksection(".multiboot")` 299 | - There're no official docs for this `keyword`, yet. 300 | - Basically we're linking the `Multiboot` variable with `.multiboot` section. 301 | 302 | So now, if you try to run `zig build` you will, get the following error: 303 | ```shell 304 | LLD Link... warning(link): unexpected LLD stderr: 305 | ld.lld: warning: cannot find entry symbol _start; not setting start address 306 | ``` 307 | 308 | This means that we didn't defined the entrypoint `_start` declared on `src/linker.ld`, we'll do it in the next section: 309 | 310 | ### Entry Function 311 | Let's add the following code to our `src/main.zig`: 312 | 313 | #### Defining Start 314 | So now that we've our `multiboot` set, let's write our entrypoint `_start`: 315 | ```zig 316 | export fn _start() callconv(.Naked) noreturn { 317 | while(true){} 318 | } 319 | ``` 320 | - `export fn _start() callconv(.Naked) noreturn` 321 | - `fn`: _"declares a function."_ 322 | - `_start()`: is the name of the function. 323 | - `callconv(.Naked)`: 324 | - `callconv`: _"[...] The callconv specifier changes the calling convention of the function."_ 325 | - `.Naked`: _"The naked calling convention makes a function not have any function prologue or epilogue. This can be useful when integrating with assembly. [...]"_ 326 | - `noreturn`: sets the function as non-returnable, so it'll run until the program/process is terminated. 327 | 328 | Now, let's try building it again: 329 | 1. > $ zig build 330 | 2. > $ qemu-system-x86_64 -kernel zig-out/bin/zig-os 331 | ```shell 332 | VNC server running on ::1:5900 333 | ``` 334 | Oh, that's something else :) 335 | Now, in another terminal run: 336 | > $ vinagre localhost:5900 337 | 338 | This should open a window with our kernel running, well, it's doing much but it's something. 339 | 340 | ## Printing Hello 341 | Now that we've our kernel ~barely~ "working", it's time to write something in the terminal, let's create the file: `src/terminal/terminal.zig` 342 | 343 | ### Colors 344 | First we need to define some constants: 345 | ```zig 346 | // src/terminal/terminal.zig 347 | // Because this is a 16 bits terminal, we need to define the 16 colors 348 | const VgaColor = u8; 349 | const VGA_COLOR_BLACK = 0; 350 | const VGA_COLOR_BLUE = 1; 351 | const VGA_COLOR_GREEN = 2; 352 | const VGA_COLOR_CYAN = 3; 353 | const VGA_COLOR_RED = 4; 354 | const VGA_COLOR_MAGENTA = 5; 355 | const VGA_COLOR_BROWN = 6; 356 | const VGA_COLOR_LIGHT_GREY = 7; 357 | const VGA_COLOR_DARK_GREY = 8; 358 | const VGA_COLOR_LIGHT_BLUE = 9; 359 | const VGA_COLOR_LIGHT_GREEN = 10; 360 | const VGA_COLOR_LIGHT_CYAN = 11; 361 | const VGA_COLOR_LIGHT_RED = 12; 362 | const VGA_COLOR_LIGHT_MAGENTA = 13; 363 | const VGA_COLOR_LIGHT_BROWN = 14; 364 | const VGA_COLOR_WHITE = 15; 365 | 366 | // foreground | background color. 367 | fn vgaEntryColor(fg: VgaColor, bg: VgaColor) u8 { 368 | // will build the byte representing the color. 369 | return fg | (bg << 4); 370 | } 371 | 372 | // character | color 373 | fn vgaEntry(uc: u8, color: u8) u16 { 374 | var c: u16 = color; 375 | 376 | // build the 2 bytes representing the printable caracter w/ EntryColor. 377 | return uc | (c << 8); 378 | } 379 | ``` 380 | 381 | ### Dimensions 382 | We need to define somehow how many rows/columns the terminal will have, so before we build our terminal, let's define some constants for it: 383 | ```zig 384 | // src/terminal/terminal.zig 385 | const VGA_WIDTH = 80; 386 | const VGA_HEIGHT = 45; 387 | ``` 388 | 389 | ### Terminal Struct 390 | First, we declared the struct: 391 | ```zig 392 | // src/terminal/terminal.zig 393 | pub const terminal = struct {} 394 | ``` 395 | - `pub` - Keyword to define that it's public, allowing us to import it from `src/main.zig` 396 | 397 | But an empty struct, won't do nothing, let's start filling it with code: 398 | 399 | #### Attributes 400 | ```zig 401 | // row / column are variable to allow us to keep up 402 | // with the current position in the buffer 403 | var row: usize = 0; 404 | var column: usize = 0; 405 | 406 | // color will be a u16 value that represents the fore/back ground color 407 | // of our terminal. 408 | var color = vgaEntryColor(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); 409 | 410 | // buffer is the data in our screen, so if we want to print "Hello, World!" 411 | // we'll need to store it in our buffer 412 | const buffer = @intToPtr([*]volatile u16, 0xB8000); 413 | ``` 414 | 415 | - `const buffer = @intToPtr([*]volatile u16, 0xB8000);` 416 | - `const buffer`: declaring a constant named _"buffer"_ 417 | - `@intToPtr`: 418 | - _"Converts an integer to a pointer. [...]"_ 419 | - `@intToPtr(comptime DestType: type, address: usize) DestType` 420 | - `[*] volatile u16`: 421 | - `[*] T` _"many-item pointer to unknown number of items."_ of type `T` 422 | - `volatile`: _"can be used to denote loads or stores of a pointer have side effects. It can also modify an inline assembly expression to denote it has side effects."_ 423 | - `u16`: 16 bits - 2 bytes 424 | - `0xB8000`: _"[...] The text screen video memory for colour monitors resides at 0xB8000 [...]"_ 425 | 426 | #### Functions 427 | ```zig 428 | // initialize fills the current buffer with the empty character(' '). 429 | pub fn initialize() void { 430 | var y: usize = 0; 431 | while (y < VGA_HEIGHT) : (y += 1) { 432 | var x: usize = 0; 433 | while (x < VGA_WIDTH) : (x += 1) { 434 | putCharAt(' ', color, x, y); 435 | } 436 | } 437 | } 438 | 439 | // setColor updates the current color value. 440 | fn setColor(new_color: u8) void { 441 | color = new_color; 442 | } 443 | 444 | // putCharAt receives: 445 | // - `char` the u8. 446 | // - `char_color` the char color. 447 | // - `x` - the column position 448 | // - `y` - the row position 449 | // and with that, stores the `char` in the given buffer [index] 450 | fn putCharAt(char: u8, char_color: u8, x: usize, y: usize) void { 451 | const index = y * VGA_WIDTH + x; 452 | buffer[index] = vgaEntry(char, char_color); 453 | } 454 | 455 | // putChar will store a byte in the current buffer position 456 | // and then update the current position to the next one. 457 | fn putChar(c: u8) void { 458 | putCharAt(c, color, column, row); 459 | column += 1; 460 | if (column == VGA_WIDTH) { 461 | column = 0; 462 | row += 1; 463 | if (row == VGA_HEIGHT) 464 | row = 0; 465 | } 466 | } 467 | 468 | // write receives an array of data and stores in the buffer. 469 | pub fn write(data: []const u8) void { 470 | for (data) |c| 471 | putChar(c); 472 | } 473 | ``` 474 | 475 | #### Implementing Terminal 476 | In `src/main.zig`: 477 | ```zig 478 | // import our terminal module. 479 | const term = @import("terminal/terminal.zig").terminal; 480 | 481 | 482 | export fn _start() callconv(.Naked) noreturn { 483 | // initialize the terminal 484 | term.initialize(); 485 | // write data into the buffer. 486 | term.write("Hello, World!"); 487 | while (true) {} 488 | } 489 | ``` 490 | 491 | #### Printing: 492 | DISCLAIMER: If you get stuck in terminal, try to Press `CTRL+Alt+2` and then type `quit` and enter. 493 | 494 | 1. > $ zig build 495 | 2. > $ qemu-system-x86_64 -kernel zig-out/bin/zig-os 496 | 497 | This should pop open a window with `Hello, World!` Written on it 498 | 499 | ### Next Steps 500 | Well, we're far away from having a working kernel, we don't even have a working shell, so maybe that's something that we could in the next chapter. 501 | 502 | [Chapter 3 - Keyboard Input](./03_keyboard_input.md) 503 | 504 | ## References 505 | - [PVH](https://xenbits.xen.org/docs/4.6-testing/misc/pvh.html) 506 | - [HVM](https://docs.rightscale.com/faq/What_is_Hardware_Virtual_Machine_or_HVM.html) 507 | - [PV](https://wiki.xenproject.org/wiki/Paravirtualization_(PV)) 508 | - [PV HVM](https://www.linux.org/threads/hardware-virtual-machine-hvm-and-paravirtualization-pv.12475/) 509 | - [PV HVM (AWS)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/virtualization_types.html) 510 | - [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) 511 | - [Linker Script](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html) 512 | - [LD File](https://fileinfo.com/extension/ld) 513 | - [OSDev ORG](https://wiki.osdev.org/Main_Page) 514 | - [0xB8000](https://wiki.osdev.org/Printing_To_Screen) 515 | --------------------------------------------------------------------------------