Zig Bare Bones
This page or section is a stub. You can help the wiki by accurately contributing to it. |
This tutorial needs to explain what the code does as tutorials are not just copy paste. You can help out by editing this page to include more context to what the code does. |
In this tutorial, we'll make a simple hello world kernel in Zig.
Prerequisites
First off, you'll need:
Code
If you done setting up all of the prerequisites above, we can now write some code for our kernel
build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
var disabled_features = std.Target.Cpu.Feature.Set.empty;
var enabled_features = std.Target.Cpu.Feature.Set.empty;
disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.mmx));
disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse));
disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse2));
disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx));
disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx2));
enabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.soft_float));
const target_query = std.Target.Query{
.cpu_arch = std.Target.Cpu.Arch.x86,
.os_tag = std.Target.Os.Tag.freestanding,
.abi = std.Target.Abi.none,
.cpu_features_sub = disabled_features,
.cpu_features_add = enabled_features,
};
const optimize = b.standardOptimizeOption(.{});
const kernel = b.addExecutable(.{
.name = "kernel.elf",
.root_source_file = b.path("src/main.zig"),
.target = b.resolveTargetQuery(target_query),
.optimize = optimize,
.code_model = .kernel,
});
kernel.setLinkerScript(b.path("src/linker.ld"));
b.installArtifact(kernel);
const kernel_step = b.step("kernel", "Build the kernel");
kernel_step.dependOn(&kernel.step);
}
src/main.zig
const console = @import("./console.zig");
const ALIGN = 1 << 0;
const MEMINFO = 1 << 1;
const MAGIC = 0x1BADB002;
const FLAGS = ALIGN | MEMINFO;
const MultibootHeader = packed struct {
magic: i32 = MAGIC,
flags: i32,
checksum: i32,
padding: u32 = 0,
};
export var multiboot: MultibootHeader align(4) linksection(".multiboot") = .{
.flags = FLAGS,
.checksum = -(MAGIC + FLAGS),
};
var stack_bytes: [16 * 1024]u8 align(16) linksection(".bss") = undefined;
// We specify that this function is "naked" to let the compiler know
// not to generate a standard function prologue and epilogue, since
// we don't have a stack yet.
export fn _start() callconv(.Naked) noreturn {
// We use inline assembly to set up the stack before jumping to
// our kernel main.
asm volatile (
\\ movl %[stack_top], %%esp
\\ movl %%esp, %%ebp
\\ call %[kmain:P]
:
// The stack grows downwards on x86, so we need to point ESP
// to one element past the end of `stack_bytes`.
//
// Unfortunately, we can't just compute `&stack_bytes[stack_bytes.len]`,
// as the Zig compiler will notice the out-of-bounds access
// at compile-time and throw an error.
//
// We can instead take the start address of `stack_bytes` and
// add the size of the array to get the one-past-the-end
// pointer. However, Zig disallows pointer arithmetic on all
// pointer types except "multi-pointers" `[*]`, so we must cast
// to that type first.
//
// Finally, we pass the whole expression as an input operand
// with the "immediate" constraint to force the compiler to
// encode this as an absolute address. This prevents the
// compiler from doing unnecessary extra steps to compute
// the address at runtime (especially in Debug mode), which
// could possibly clobber registers that are specified by
// multiboot to hold special values (e.g. EAX).
: [stack_top] "i" (@as([*]align(16) u8, @ptrCast(&stack_bytes)) + @sizeOf(@TypeOf(stack_bytes))),
// We let the compiler handle the reference to kmain by passing it as an input operand as well.
[kmain] "X" (&kmain),
:
);
}
fn kmain() callconv(.C) void {
console.initialize();
console.puts("Hello Zig Kernel!");
}
src/console.zig
const fmt = @import("std").fmt;
const Writer = @import("std").io.Writer;
const VGA_WIDTH = 80;
const VGA_HEIGHT = 25;
const VGA_SIZE = VGA_WIDTH * VGA_HEIGHT;
pub const ConsoleColors = enum(u8) {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
LightMagenta = 13,
LightBrown = 14,
White = 15,
};
var row: usize = 0;
var column: usize = 0;
var color = vgaEntryColor(ConsoleColors.LightGray, ConsoleColors.Black);
var buffer = @as([*]volatile u16, @ptrFromInt(0xB8000));
fn vgaEntryColor(fg: ConsoleColors, bg: ConsoleColors) u8 {
return @intFromEnum(fg) | (@intFromEnum(bg) << 4);
}
fn vgaEntry(uc: u8, new_color: u8) u16 {
const c: u16 = new_color;
return uc | (c << 8);
}
pub fn initialize() void {
clear();
}
pub fn setColor(new_color: u8) void {
color = new_color;
}
pub fn clear() void {
@memset(buffer[0..VGA_SIZE], vgaEntry(' ', color));
}
pub fn putCharAt(c: u8, new_color: u8, x: usize, y: usize) void {
const index = y * VGA_WIDTH + x;
buffer[index] = vgaEntry(c, new_color);
}
pub fn putChar(c: u8) void {
putCharAt(c, color, column, row);
column += 1;
if (column == VGA_WIDTH) {
column = 0;
row += 1;
if (row == VGA_HEIGHT)
row = 0;
}
}
pub fn puts(data: []const u8) void {
for (data) |c|
putChar(c);
}
pub const writer = Writer(void, error{}, callback){ .context = {} };
fn callback(_: void, string: []const u8) error{}!usize {
puts(string);
return string.len;
}
pub fn printf(comptime format: []const u8, args: anytype) void {
fmt.format(writer, format, args) catch unreachable;
}
src/linker.ld
ENTRY(_start)
SECTIONS {
. = 2M;
.text : ALIGN(4K) {
/* We need to specify KEEP to prevent the linker from garbage-collecting the multiboot section. */
KEEP(*(.multiboot))
*(.text)
}
.rodata : ALIGN(4K) {
*(.rodata)
}
.data : ALIGN(4K) {
*(.data)
}
.bss : ALIGN(4K) {
*(COMMON)
*(.bss)
}
}
Build
Now that our kernel code is done, we'll now build our kernel by running the command below:
$ zig build
Verifying Multiboot
If the header isn't valid, GRUB will give an error that it can't find a Multiboot header when you try to boot it. This code fragment will help you diagnose such cases:
grub-file --is-x86-multiboot zig-out/bin/kernel.elf
grub-file
is quiet but will exit 0 (successfully) if it is a valid multiboot kernel and exit 1 (unsuccessfully) otherwise. You can type echo $?
in your shell immediately afterwards to see the exit status.
Booting the Kernel
You can easily create a bootable CD-ROM image containing the GRUB bootloader and your kernel using the program grub-mkrescue
. You may need to install the GRUB utility programs and the program xorriso
(version 0.5.6 or higher). First you should create a file called grub.cfg
containing the contents:
menuentry "Zig Bare Bones" {
multiboot /boot/kernel.elf
}
Note that the braces must be placed as shown here. You can now create a bootable image of your operating system by typing these commands:
mkdir -p isodir/boot/grub
cp zig-out/bin/kernel.elf isodir/boot/kernel.elf
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o kernel.iso isodir
Warning: GNU GRUB, the bootloader used by grub-mkrescue
, is licensed under the GNU General Public License. Your iso file contains copyrighted material under that license and redistributing it in violation of the GPL constitutes copyright infringement. The GPL requires you publish the source code corresponding to the bootloader. You need to get the exact source package corresponding to the GRUB package you have installed from your distribution, at the time grub-mkrescue
is invoked (as distro packages are occasionally updated). You then need to publish that source code along with your ISO to satisfy the GPL. Alternative, you can build GRUB from source code yourself. Clone the latest GRUB git from savannah (do not use their last release from 2012, it's severely out of date). Run autogen.sh, ./configure and make dist. That makes a GRUB tarball. Extract it somewhere, then build GRUB from it, and install it in a isolated prefix. Add that to your PATH and ensure its grub-mkrescue
program is used to produce your iso. Then publish the GRUB tarball of your own making along with your OS release. You're not required to publish the source code of your OS at all, only the code for the bootloader that's inside the iso.
Testing your operating system (QEMU)
In this tutorial, we will be using QEMU. You can also use other virtual machines if you please. Simply adding the ISO to the CD drive of an empty virtual machine will do the trick.
Install QEMU from your repositories, and then use the following command to start your new operating system.
qemu-system-i386 -cdrom kernel.iso
Additionally, QEMU supports booting multiboot kernels directly without bootable medium:
qemu-system-i386 -kernel kernel.elf