initial commit

This commit is contained in:
Loïc Grégoire
2026-03-21 22:46:54 -04:00
parent cf7df37498
commit 9e065ee023
45 changed files with 4430 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
const std = @import("std");
const builtin = @import("builtin");
pub const Config = std.StringHashMap([]const u8);
pub const Platform = struct {
arch: std.Target.Cpu.Arch,
os: std.Target.Os.Tag,
};
pub fn parseConfig(b: *std.Build) !Config {
const file = try std.Io.Dir.cwd().readFileAlloc(b.graph.io, ".config", b.allocator, std.Io.Limit.unlimited);
var lines = std.mem.splitScalar(u8, file, '\n');
var map = std.StringHashMap([]const u8).init(b.allocator);
while (lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \r\t");
if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) {
continue;
}
var kv = std.mem.splitScalar(u8, trimmed, '=');
const key = kv.first();
const value = kv.rest();
try map.put(key, value);
}
return map;
}
pub fn configStep(b: *std.Build) *std.Build.Step {
return &b.addSystemCommand(&.{ "bash", "build/menuconfig" }).step;
}
fn isInteger(str: []const u8) bool {
_ = std.fmt.parseInt(i64, str, 10) catch return false;
return true;
}
pub fn generateConfig(b: *std.Build, config: Config) !*std.Build.Step.WriteFile {
var out = std.Io.Writer.Allocating.init(b.allocator);
var it = config.iterator();
while (it.next()) |entry| {
const key = entry.key_ptr.*;
const value = entry.value_ptr.*;
if (std.mem.eql(u8, value, "y")) {
try out.writer.print("pub const {s}: bool = true;\n", .{key});
} else if (isInteger(value)) {
try out.writer.print("pub const {s}: i64 = {s};\n", .{ key, value });
} else {
try out.writer.print("pub const {s} = \"{s}\";\n", .{ key, value });
}
}
const wf = b.addWriteFiles();
_ = wf.add("config.gen.zig", out.written());
_ = wf.addCopyFile(b.path(".config"), ".config");
return wf;
}
pub fn getPlatform(config: Config) !Platform {
const keys = .{
.{ "CONFIG_ARCH_AMD64", Platform{ .arch = .x86_64, .os = .freestanding } },
.{ "CONFIG_ARCH_AARCH64", Platform{ .arch = .aarch64, .os = .freestanding } },
.{ "CONFIG_ARCH_RISCV64", Platform{ .arch = .riscv64, .os = .freestanding } },
.{ "CONFIG_ARCH_UM", Platform{ .arch = builtin.cpu.arch, .os = .linux } },
};
inline for (keys) |entry| {
if (config.get(entry[0])) |val| {
if (std.mem.eql(u8, val, "y")) return entry[1];
}
}
return error.NoArchConfigured;
}
+111
View File
@@ -0,0 +1,111 @@
const std = @import("std");
const Arch = std.Target.Cpu.Arch;
fn addLimineTool(b: *std.Build, limine: *std.Build.Dependency) *std.Build.Step.Compile {
const tool = b.addExecutable(.{ .name = "limine", .root_module = b.addModule("limine", .{
.target = b.graph.host,
.link_libc = true,
}) });
tool.root_module.addCSourceFile(.{ .file = limine.path("limine.c") });
return tool;
}
pub const IsoResult = struct {
step: *std.Build.Step,
path: std.Build.LazyPath,
};
pub fn addIso(
b: *std.Build,
arch: Arch,
image_name: []const u8,
kernel: *std.Build.Step.Compile,
limine: *std.Build.Dependency,
) IsoResult {
const limine_tool = addLimineTool(b, limine);
const kernel_bin = kernel.getEmittedBin();
const prep_script =
\\set -euo pipefail
\\kernel="$1"
\\iso_root="$2"
\\limine_conf="$3"
\\limine_dir="$4"
\\arch="$5"
\\rm -rf "$iso_root"
\\mkdir -p "$iso_root/boot/limine" "$iso_root/EFI/BOOT"
\\cp "$kernel" "$iso_root/boot/kernel"
\\cp "$limine_conf" "$iso_root/boot/limine/limine.conf"
\\case "$arch" in
\\ x86_64)
\\ cp "$limine_dir/limine-bios.sys" "$limine_dir/limine-bios-cd.bin" "$limine_dir/limine-uefi-cd.bin" "$iso_root/boot/limine/"
\\ cp "$limine_dir/BOOTX64.EFI" "$limine_dir/BOOTIA32.EFI" "$iso_root/EFI/BOOT/"
\\ ;;
\\ aarch64)
\\ cp "$limine_dir/limine-uefi-cd.bin" "$iso_root/boot/limine/"
\\ cp "$limine_dir/BOOTAA64.EFI" "$iso_root/EFI/BOOT/"
\\ ;;
\\ riscv64)
\\ cp "$limine_dir/limine-uefi-cd.bin" "$iso_root/boot/limine/"
\\ cp "$limine_dir/BOOTRISCV64.EFI" "$iso_root/EFI/BOOT/"
\\ ;;
\\ loongarch64)
\\ cp "$limine_dir/limine-uefi-cd.bin" "$iso_root/boot/limine/"
\\ cp "$limine_dir/BOOTLOONGARCH64.EFI" "$iso_root/EFI/BOOT/"
\\ ;;
\\esac
;
const iso_root = b.addTempFiles();
const prep = b.addSystemCommand(&.{ "bash", "-ceu", prep_script, "--" });
prep.addFileArg(kernel_bin);
prep.addDirectoryArg(iso_root.getDirectory());
prep.addFileArg(b.path("build/limine.conf"));
prep.addDirectoryArg(limine.path("."));
prep.addArg(@tagName(arch));
prep.step.dependOn(&iso_root.step);
const mkiso = b.addSystemCommand(&.{ "xorriso", "-as", "mkisofs", "-R", "-r", "-J" });
switch (arch) {
.x86_64 => mkiso.addArgs(&.{
"-b", "boot/limine/limine-bios-cd.bin",
"-no-emul-boot", "-boot-load-size",
"4", "-boot-info-table",
"-hfsplus", "-apm-block-size",
"2048", "--efi-boot",
"boot/limine/limine-uefi-cd.bin", "-efi-boot-part",
"--efi-boot-image", "--protective-msdos-label",
}),
else => mkiso.addArgs(&.{
"-hfsplus",
"-apm-block-size",
"2048",
"--efi-boot",
"boot/limine/limine-uefi-cd.bin",
"-efi-boot-part",
"--efi-boot-image",
"--protective-msdos-label",
}),
}
mkiso.addDirectoryArg(iso_root.getDirectory());
mkiso.addArg("-o");
const iso_out = mkiso.addOutputFileArg(b.fmt("{s}.iso", .{image_name}));
mkiso.step.dependOn(&prep.step);
if (arch == .x86_64) {
const bios_install = b.addRunArtifact(limine_tool);
bios_install.addArg("bios-install");
bios_install.addFileArg(iso_out);
bios_install.step.dependOn(&mkiso.step);
}
const install_iso = b.addInstallFile(iso_out, b.fmt("{s}.iso", .{image_name}));
install_iso.step.dependOn(&mkiso.step);
return .{
.step = &install_iso.step,
.path = iso_out,
};
}
Executable
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
# Simple kernel symbol dumper
# Usage: ksyms.py --input <kernel binary> --output <output file>
import sys
import subprocess
import argparse
excluded_symbols = ['text_start_addr', 'text_end_addr', 'rodata_start_addr', 'rodata_end_addr', 'data_start_addr', 'data_end_addr']
def parse_nm_line(line: str) -> tuple[int, str]:
parts = line.split()
if len(parts) < 3:
return None
addr_str, type, name = parts[0], parts[1], parts[2]
# Only get symbols in text
if type.lower() != 't':
return None
if name in excluded_symbols:
return None
addr = int(addr_str, 16)
return addr, name
def run_nm(binary: str) -> list[str]:
result = subprocess.run(["nm", "-n", binary], capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"nm failed: {result.stderr}")
return result.stdout.splitlines()
parser = argparse.ArgumentParser(description="Generate kernel symbols from a binary")
parser.add_argument("--input", help="Input kernel binary")
parser.add_argument("--output", help="Output C file")
parser.add_argument("--empty", action="store_true", help="Generate empty ksyms")
args = parser.parse_args()
if args.empty:
with open(args.output, 'w') as f:
f.write("// Auto-generated by build/ksyms.py - do not edit\n\n")
f.write("pub const KSym = struct {\n addr: u64,\n name: []const u8,\n};\n\n")
f.write("pub const ksyms: []const KSym = &.{};\n")
sys.exit(0)
lines = run_nm(args.input)
symbols = []
for line in lines:
parsed = parse_nm_line(line)
if parsed:
symbols.append(parsed)
# Sort by address
symbols.sort(key=lambda x: x[0])
with open(args.output, 'w') as f:
f.write("// Auto-generated by build/ksyms.py - do not edit\n\n")
f.write("pub const KSym = struct {\n addr: u64,\n name: []const u8,\n};\n\n")
f.write("pub const ksyms: []const KSym = &.{\n")
for addr, name in symbols:
f.write(f" .{{ .addr = 0x{addr:016x}, .name = \"{name}\" }},\n")
f.write("};\n")
+11
View File
@@ -0,0 +1,11 @@
# Timeout in seconds that Limine will use before automatically booting.
timeout: 3
# The entry name that will be displayed in the boot menu.
/Limine Template (Zig)
# We use the Limine boot protocol.
protocol: limine
# Path to the kernel to boot. boot():/ represents the partition on which limine.conf is located.
path: boot():/boot/kernel
+75
View File
@@ -0,0 +1,75 @@
/* Tell the linker that we want an aarch64 ELF64 output file */
OUTPUT_FORMAT(elf64-littleaarch64)
/* We want the symbol kmain to be our entry point */
ENTRY(kmain)
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions; this also allows us to exert more control over the linking */
/* process. */
PHDRS
{
limine_requests PT_LOAD;
text PT_LOAD;
rodata PT_LOAD;
data PT_LOAD;
}
SECTIONS
{
/* We want to be placed in the topmost 2GiB of the address space, for optimisations */
/* and because that is what the Limine spec mandates. */
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
KEEP(*(.limine_requests_start))
KEEP(*(.limine_requests))
KEEP(*(.limine_requests_end))
} :limine_requests
/* Move to the next memory page for .text */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.text : {
*(.text .text.*)
} :text
/* Move to the next memory page for .rodata */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* Add a .note.gnu.build-id output section in case a build ID flag is added to the */
/* linker command. */
.note.gnu.build-id : {
*(.note.gnu.build-id)
} :rodata
/* Move to the next memory page for .data */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : {
*(.data .data.*)
} :data
/* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
/* unnecessary zeros will be written to the binary. */
/* If you need, for example, .init_array and .fini_array, those should be placed */
/* above this. */
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
/DISCARD/ : {
*(.eh_frame*)
*(.note .note.*)
}
}
+75
View File
@@ -0,0 +1,75 @@
/* Tell the linker that we want a loongarch64 ELF64 output file */
OUTPUT_FORMAT(elf64-loongarch)
/* We want the symbol kmain to be our entry point */
ENTRY(kmain)
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions; this also allows us to exert more control over the linking */
/* process. */
PHDRS
{
limine_requests PT_LOAD;
text PT_LOAD;
rodata PT_LOAD;
data PT_LOAD;
}
SECTIONS
{
/* We want to be placed in the topmost 2GiB of the address space, for optimisations */
/* and because that is what the Limine spec mandates. */
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
KEEP(*(.limine_requests_start))
KEEP(*(.limine_requests))
KEEP(*(.limine_requests_end))
} :limine_requests
/* Move to the next memory page for .text */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.text : {
*(.text .text.*)
} :text
/* Move to the next memory page for .rodata */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* Add a .note.gnu.build-id output section in case a build ID flag is added to the */
/* linker command. */
.note.gnu.build-id : {
*(.note.gnu.build-id)
} :rodata
/* Move to the next memory page for .data */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : {
*(.data .data.*)
} :data
/* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
/* unnecessary zeros will be written to the binary. */
/* If you need, for example, .init_array and .fini_array, those should be placed */
/* above this. */
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
/DISCARD/ : {
*(.eh_frame*)
*(.note .note.*)
}
}
+75
View File
@@ -0,0 +1,75 @@
/* Tell the linker that we want a riscv64 ELF64 output file */
OUTPUT_FORMAT(elf64-littleriscv)
/* We want the symbol kmain to be our entry point */
ENTRY(kmain)
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions; this also allows us to exert more control over the linking */
/* process. */
PHDRS
{
limine_requests PT_LOAD;
text PT_LOAD;
rodata PT_LOAD;
data PT_LOAD;
}
SECTIONS
{
/* We want to be placed in the topmost 2GiB of the address space, for optimisations */
/* and because that is what the Limine spec mandates. */
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
KEEP(*(.limine_requests_start))
KEEP(*(.limine_requests))
KEEP(*(.limine_requests_end))
} :limine_requests
/* Move to the next memory page for .text */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.text : {
*(.text .text.*)
} :text
/* Move to the next memory page for .rodata */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* Add a .note.gnu.build-id output section in case a build ID flag is added to the */
/* linker command. */
.note.gnu.build-id : {
*(.note.gnu.build-id)
} :rodata
/* Move to the next memory page for .data */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : {
*(.data .data.*)
} :data
/* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
/* unnecessary zeros will be written to the binary. */
/* If you need, for example, .init_array and .fini_array, those should be placed */
/* above this. */
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
/DISCARD/ : {
*(.eh_frame*)
*(.note .note.*)
}
}
+75
View File
@@ -0,0 +1,75 @@
/* Tell the linker that we want an x86_64 ELF64 output file */
OUTPUT_FORMAT(elf64-x86-64)
/* We want the symbol kmain to be our entry point */
ENTRY(kmain)
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions; this also allows us to exert more control over the linking */
/* process. */
PHDRS
{
limine_requests PT_LOAD;
text PT_LOAD;
rodata PT_LOAD;
data PT_LOAD;
}
SECTIONS
{
/* We want to be placed in the topmost 2GiB of the address space, for optimisations */
/* and because that is what the Limine spec mandates. */
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
KEEP(*(.limine_requests_start))
KEEP(*(.limine_requests))
KEEP(*(.limine_requests_end))
} :limine_requests
/* Move to the next memory page for .text */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.text : {
*(.text .text.*)
} :text
/* Move to the next memory page for .rodata */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* Add a .note.gnu.build-id output section in case a build ID flag is added to the */
/* linker command. */
.note.gnu.build-id : {
*(.note.gnu.build-id)
} :rodata
/* Move to the next memory page for .data */
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : {
*(.data .data.*)
} :data
/* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
/* unnecessary zeros will be written to the binary. */
/* If you need, for example, .init_array and .fini_array, those should be placed */
/* above this. */
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
/DISCARD/ : {
*(.eh_frame*)
*(.note .note.*)
}
}
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
frontend="${FRONTEND:-kconfig-nconf}"
has_kconfig_frontends=true
if ! command -v kconfig-mconf &> /dev/null
then
has_kconfig_frontends=false
fi
if [ "$has_kconfig_frontends" = false ]; then
if ! command -v python3 -m menuconfig -h &> /dev/null
then
echo "No Kconfig frontend found. Please install kconfig-frontends or python3-kconfiglib"
exit 1
fi
python3 -m menuconfig
else
$frontend Kconfig
fi
+69
View File
@@ -0,0 +1,69 @@
const std = @import("std");
const config = @import("config.zig");
const image = @import("image.zig");
const QemuArgs = struct {
suffix: []u8,
args: [][]u8,
};
pub fn addRun(b: *std.Build, kernel: *std.Build.Step.Compile, plat: config.Platform) void {
const run_step = b.step("run", "Run the kernel");
const uefi = b.option(bool, "uefi", "Boot with UEFI firmware") orelse false;
const debug = b.option(bool, "debug", "Boot in debug mode") orelse false;
const extra_qemu_args = b.option([]const []const u8, "qemu-args", "Extra arguments to pass to QEMU") orelse &.{};
if (plat.os == .freestanding) {
const limine = b.dependency("limine", .{});
const ovmf = b.dependency("ovmf", .{});
const iso_out = image.addIso(b, plat.arch, "myos", kernel, limine);
const qemu_bin = switch (plat.arch) {
.x86_64 => "qemu-system-x86_64",
.aarch64 => "qemu-system-aarch64",
.riscv64 => "qemu-system-riscv64",
else => "qemu-system-x86_64",
};
const ovmf_fd = switch (plat.arch) {
.x86_64 => "ovmf-code-x86_64.fd",
.aarch64 => "ovmf-code-aarch64.fd",
.riscv64 => "ovmf-code-riscv64.fd",
else => "ovmf-code-x86_64.fd",
};
const qemu = b.addSystemCommand(&.{qemu_bin});
if (plat.arch != .x86_64 or uefi) {
qemu.addArg("-drive");
qemu.addPrefixedFileArg("if=pflash,format=raw,readonly=on,file=", ovmf.path(ovmf_fd));
}
qemu.addArgs(extra_qemu_args);
qemu.addArg("-cdrom");
qemu.addFileArg(iso_out.path);
if (debug) {
qemu.addArgs(&.{
"-serial", "stdio",
"-d", "int",
"-D", "qemu.log",
"-no-reboot", "-no-shutdown",
});
}
switch (plat.arch) {
.x86_64 => {
qemu.addArgs(&.{ "-debugcon", "stdio" });
if (!debug) qemu.addArgs(&.{ "-enable-kvm", "-cpu", "host" });
},
.aarch64 => qemu.addArgs(&.{ "-machine", "virt", "-cpu", "cortex-a57" }),
.riscv64 => qemu.addArgs(&.{ "-machine", "virt" }),
else => {},
}
run_step.dependOn(&qemu.step);
} else {
run_step.dependOn(&b.addRunArtifact(kernel).step);
}
}