Zig Bare Bones: Difference between revisions
[unchecked revision] | [unchecked revision] |
No edit summary |
m (fix syntax highlighting error) |
||
(11 intermediate revisions by 3 users not shown) | |||
Line 6: | Line 6: | ||
== Prerequisites == |
== Prerequisites == |
||
First off, you'll need: |
First off, you'll need: |
||
* The [[Zig]] compiler, at least version 0. |
* The [[Zig]] compiler, at least version 0.12.0 |
||
* [[GRUB]] as our bootloader to boot the kernel |
* [[GRUB]] as our bootloader to boot the kernel |
||
Line 14: | Line 14: | ||
=== build.zig === |
=== build.zig === |
||
< |
<syntaxhighlight lang="zig"> |
||
const std = @import("std"); |
const std = @import("std"); |
||
const Builder = @import("std").build.Builder; |
const Builder = @import("std").build.Builder; |
||
Line 42: | Line 42: | ||
}; |
}; |
||
const |
const optimize = b.standardOptimizeOption(.{}); |
||
const kernel = b.addExecutable( |
const kernel = b.addExecutable(.{ |
||
kernel. |
.name = "kernel.elf", |
||
.root_source_file = b.path("src/main.zig"), |
|||
kernel.setBuildMode(mode); |
|||
.target = target, |
|||
⚫ | |||
.optimize = optimize, |
|||
.code_model = .kernel, |
|||
kernel.install(); |
|||
}); |
|||
⚫ | |||
b.installArtifact(kernel); |
|||
const kernel_step = b.step("kernel", "Build the kernel"); |
const kernel_step = b.step("kernel", "Build the kernel"); |
||
Line 91: | Line 94: | ||
run_step.dependOn(&run_cmd.step); |
run_step.dependOn(&run_cmd.step); |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
=== src/main.zig === |
=== src/main.zig === |
||
< |
<syntaxhighlight lang="zig"> |
||
const console = @import("console.zig"); |
const console = @import("console.zig"); |
||
Line 126: | Line 129: | ||
console.puts("Hello world!"); |
console.puts("Hello world!"); |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
=== src/console.zig === |
=== src/console.zig === |
||
< |
<syntaxhighlight lang="zig"> |
||
const fmt = @import("std").fmt; |
const fmt = @import("std").fmt; |
||
const mem = @import("std").mem; |
|||
const Writer = @import("std").io.Writer; |
const Writer = @import("std").io.Writer; |
||
Line 160: | Line 162: | ||
var column: usize = 0; |
var column: usize = 0; |
||
var color = vgaEntryColor(ConsoleColors.LightGray, ConsoleColors.Black); |
var color = vgaEntryColor(ConsoleColors.LightGray, ConsoleColors.Black); |
||
var buffer = @ |
var buffer = @as([*]volatile u16, @ptrFromInt(0xB8000)); |
||
fn vgaEntryColor(fg: ConsoleColors, bg: ConsoleColors) u8 { |
fn vgaEntryColor(fg: ConsoleColors, bg: ConsoleColors) u8 { |
||
Line 181: | Line 183: | ||
pub fn clear() void { |
pub fn clear() void { |
||
@memset(u16, buffer[0..VGA_SIZE], vgaEntry(' ', color)); |
|||
} |
} |
||
Line 215: | Line 217: | ||
fmt.format(writer, format, args) catch unreachable; |
fmt.format(writer, format, args) catch unreachable; |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
=== src/linker.ld === |
=== src/linker.ld === |
||
A [[linker script]] is also needed. This file tells the linker to place our code at the base address of 1M. In general user-space programming, code are placed at much higher areas, but that requires [[virtual memory]] to be available, or else only few computers could provide such a large physical memory space. |
|||
<source lang="asm"> |
|||
We also asks the linker to place <code>.multiboot</code> section at first, because the Multiboot specification says that the Multiboot header must be at the first 8KiB of the kernel file. |
|||
<pre> |
|||
ENTRY(_start) |
ENTRY(_start) |
||
Line 224: | Line 230: | ||
. = 1M; |
. = 1M; |
||
⚫ | |||
KEEP(*(.multiboot)) |
|||
} |
|||
.text : ALIGN(4K) { |
.text : ALIGN(4K) { |
||
⚫ | |||
*(.text) |
*(.text) |
||
} |
} |
||
Line 242: | Line 251: | ||
} |
} |
||
} |
} |
||
</ |
</pre> |
||
=== src/grub.cfg === |
=== src/grub.cfg === |
||
Finally, the last thing you need is a GRUB configuration, which tells the GRUB bootloader how to boot our kernel. |
|||
<source lang="c"> |
|||
<code>menuentry</code> adds a menu entry to the screen. When you press ENTER in the GRUB menu, GRUB will run the commands in the menu block. |
|||
<code>multiboot</code> asks GRUB to load our kernel from <code>/boot/kernel.elf</code>, and GRUB automatically runs <code>boot</code> after the menu block, which will fire the boot. |
|||
<syntaxhighlight lang="unixconfig"> |
|||
menuentry "Zig Bare Bones" { |
menuentry "Zig Bare Bones" { |
||
multiboot /boot/kernel.elf |
multiboot /boot/kernel.elf |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
== Build == |
== Build == |
||
Now that our kernel code is done, we'll now build our kernel by running |
Now that our kernel code is done, we'll now build our kernel by running the command below: |
||
the command below: |
|||
< |
<syntaxhighlight lang="bash"> |
||
$ zig build |
$ zig build |
||
</syntaxhighlight> |
|||
</source> |
|||
To boot our kernel, simply run this command: |
To boot our kernel, simply run this command: |
||
< |
<syntaxhighlight lang="bash"> |
||
$ zig build run |
$ zig build run |
||
</syntaxhighlight> |
|||
</source> |
|||
[[Category: Bare bones tutorials]] [[Category:Zig]] |
[[Category: Bare bones tutorials]] [[Category:Zig]] |
Latest revision as of 16:22, 16 June 2024
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");
const Builder = @import("std").build.Builder;
const Target = @import("std").Target;
const CrossTarget = @import("std").zig.CrossTarget;
const Feature = @import("std").Target.Cpu.Feature;
pub fn build(b: *Builder) void {
const features = Target.x86.Feature;
var disabled_features = Feature.Set.empty;
var enabled_features = Feature.Set.empty;
disabled_features.addFeature(@enumToInt(features.mmx));
disabled_features.addFeature(@enumToInt(features.sse));
disabled_features.addFeature(@enumToInt(features.sse2));
disabled_features.addFeature(@enumToInt(features.avx));
disabled_features.addFeature(@enumToInt(features.avx2));
enabled_features.addFeature(@enumToInt(features.soft_float));
const target = CrossTarget{
.cpu_arch = Target.Cpu.Arch.i386,
.os_tag = Target.Os.Tag.freestanding,
.abi = 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 = target,
.optimize = optimize,
.code_model = .kernel,
});
kernel.setLinkerScript(.{ .path = "src/linker.ld" });
b.installArtifact(kernel);
const kernel_step = b.step("kernel", "Build the kernel");
kernel_step.dependOn(&kernel.install_step.?.step);
const iso_dir = b.fmt("{s}/iso_root", .{b.cache_root});
const kernel_path = b.getInstallPath(kernel.install_step.?.dest_dir, kernel.out_filename);
const iso_path = b.fmt("{s}/disk.iso", .{b.exe_dir});
const iso_cmd_str = &[_][]const u8{
"/bin/sh", "-c",
std.mem.concat(b.allocator, u8, &[_][]const u8{
"mkdir -p ", iso_dir, " && ",
"cp ", kernel_path, " ", iso_dir, " && ",
"cp src/grub.cfg ", iso_dir, " && ",
"grub-mkrescue -o ", iso_path, " ", iso_dir
}) catch unreachable
};
const iso_cmd = b.addSystemCommand(iso_cmd_str);
iso_cmd.step.dependOn(kernel_step);
const iso_step = b.step("iso", "Build an ISO image");
iso_step.dependOn(&iso_cmd.step);
b.default_step.dependOn(iso_step);
const run_cmd_str = &[_][]const u8{
"qemu-system-x86_64",
"-cdrom", iso_path,
"-debugcon", "stdio",
"-vga", "virtio",
"-m", "4G",
"-machine", "q35,accel=kvm:whpx:tcg",
"-no-reboot", "-no-shutdown"
};
const run_cmd = b.addSystemCommand(run_cmd_str);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the kernel");
run_step.dependOn(&run_cmd.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,
};
export var multiboot align(4) linksection(".multiboot") = MultibootHeader{
.flags = FLAGS,
.checksum = -(MAGIC + FLAGS),
};
export var stack_bytes: [16 * 1024]u8 align(16) linksection(".bss") = undefined;
const stack_bytes_slice = stack_bytes[0..];
export fn _start() callconv(.Naked) noreturn {
@call(.{ .stack = stack_bytes_slice }, kmain, .{});
while (true) {}
}
fn kmain() void {
console.initialize();
console.puts("Hello world!");
}
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 @enumToInt(fg) | (@enumToInt(bg) << 4);
}
fn vgaEntry(uc: u8, new_color: u8) u16 {
var 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(u16, 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
A linker script is also needed. This file tells the linker to place our code at the base address of 1M. In general user-space programming, code are placed at much higher areas, but that requires virtual memory to be available, or else only few computers could provide such a large physical memory space.
We also asks the linker to place .multiboot
section at first, because the Multiboot specification says that the Multiboot header must be at the first 8KiB of the kernel file.
ENTRY(_start) SECTIONS { . = 1M; .multiboot { KEEP(*(.multiboot)) } .text : ALIGN(4K) { *(.text) } .rodata : ALIGN(4K) { *(.rodata) } .data : ALIGN(4K) { *(.data) } .bss : ALIGN(4K) { *(COMMON) *(.bss) } }
src/grub.cfg
Finally, the last thing you need is a GRUB configuration, which tells the GRUB bootloader how to boot our kernel.
menuentry
adds a menu entry to the screen. When you press ENTER in the GRUB menu, GRUB will run the commands in the menu block.
multiboot
asks GRUB to load our kernel from /boot/kernel.elf
, and GRUB automatically runs boot
after the menu block, which will fire the boot.
menuentry "Zig Bare Bones" {
multiboot /boot/kernel.elf
}
Build
Now that our kernel code is done, we'll now build our kernel by running the command below:
$ zig build
To boot our kernel, simply run this command:
$ zig build run