Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами

в 6:16, , рубрики: ACPI, fat32, kernel, multicore, OSDev, paging, SMP, x86, zig, операционные системы

Привет! 👋

Меня зовут Антон, и я хочу поделиться историей создания своей собственной операционной системы.

Это моя первая статья и первый серьезный релиз системы (v0.20). Многие разработчики мечтают написать свою операционную систему. Обычно этот путь заканчивается на выводе "Hello World" в VGA-буфер. Я тоже начинал с малого (C# COSMOS), потом уходил в Ассемблер, бросал, возвращался... Но в этот раз я решил пойти до конца.

Моей целью было не просто запустить ядро, а создать современную, масштабируемую архитектуру на современном языке.

Встречайте NovumOS. Это микроядро, написанное на Zig (95%) с минимумом Ассемблера, которое поддерживает:

  • SMP (Symmetric Multiprocessing): до 16 ядер.

  • Умный планировщик: Work-Stealing (воровство задач) вместо простых очередей.

  • Виртуальную память: PSE (4MB Huge Pages) и Demand Paging.

  • Файловую систему: FAT32 с длинными именами (LFN).

  • Скриптинг: Встроенный интерпретатор языка Nova.

Если вам интересно, как в 2026 году написать OS, не используя C, и как подружить ассемблерные трамплины с безопасным языком программирования — добро пожаловать.

Терминал и sysinfo

Терминал и sysinfo

Почему Zig?

Обычно OS пишут на C или Rust.

  • C — классика, но ручное управление памятью и макросы — это новый повод сойти с ума и получить тонну эксплоитов.

  • Rust — дает безопасность, но его Runtime и Borrow Checker в контексте голого железа иногда создают больше проблем, чем решают.

  • Zig — стал для меня золотой серединой.

    • Нет скрытых аллокаций: Я всегда знаю, где и сколько памяти выделяется.

    • Comptime: Можно вычислять таблицы прерываний или размеры структур на этапе компиляции.

    • Взаимодействие с ASM: Бесшовное. Можно писать функции на Zig и вызывать их из загрузчика на NASM.

Драйверы в NovumOS выглядят чисто и читаемо, без нагромождения макросов.

// Пример: Чтение сектора с диска (ATA PIO)
pub fn read_sector(drive: Drive, lba: u32, buffer: [*]u8) void {
    wait_bsy();
    // Настраиваем LBA адрес
    common.outb(ATA_PRIMARY_BASE + 6, 0xE0 | (@as(u8, @intFromEnum(drive)) << 4));
    common.outb(ATA_COMMAND_REG, 0x20); // READ SECTORS
    
    wait_drq();
    // Читаем 256 слов (512 байт)
    var i: usize = 0;
    while (i < 256) : (i += 1) {
        const word = common.inw(ATA_PRIMARY_BASE);
        buffer[i * 2] = @intCast(word & 0xFF);
        buffer[i * 2 + 1] = @intCast((word >> 8) & 0xFF);
    }
}
Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами - 2

SMP: Будим спящие ядра

Самым сложным вызовом стала реализация Symmetric Multiprocessing (SMP). При старте компьютера BIOS запускает только одно ядро (BSP). Остальные (AP) спят. Чтобы их разбудить, нужно отправить специальную последовательность прерываний (INIT-SIPI-SIPI) через контроллер APIC.

Ядра просыпаются в 16-битном Real Mode. Они ничего не знают о защищенном режиме.

Для них пришлось писать Трамплин (Trampoline) — маленький кусок кода на ASM, который:

  1. Загружается по адресу 0x8000 (куда могут дотянуться 16-битные ядра).

  2. Переключает процессор в Protected Mode.

  3. Настраивает стек (свой для каждого ядра).

  4. Прыгает в основное ядро на Zig.

// Пример: Атомарный спинлок для синхронизации ядер
fn spin_lock(lock: *volatile u32) void {
    // Compare-And-Swap в цикле
    while (@atomicRmw(u32, lock, .Xchg, 1, .acquire) == 1) {
        asm volatile ("pause"); // Подсказка процессору
    }
}

pub fn push_task(func: *const fn (usize) void, arg: usize) bool {
    // Находим наименее загруженное ядро
    var min_load: u32 = 0xFFFFFFFF;
    var target_core: u8 = 1;
    
    for (1..detected_cores) |i| {
        if (cores[i].task_count < min_load) {
            min_load = cores[i].task_count;
            target_core = @intCast(i);
        }
    }
    
    spin_lock(&cores[target_core].lock);
    defer spin_unlock(&cores[target_core].lock);
    // ... добавляем задачу в очередь
}
Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами - 3
Утилита top показывает выполненые задачи за всё время работы всех 4-х ядер

Утилита top показывает выполненые задачи за всё время работы всех 4-х ядер

Память и красные экраны

Ещё одной интересной задачей стало управление памятью.

Вместо классических 4КБ страниц я решил использовать Huge Pages (4 МБ).

Это позволяет одним махом замапить ядро, стек и видеопамять, снижая нагрузку на TLB процессора.

Однако, первые 4 МБ я оставил разбитыми по 4 КБ. Но зачем? Чтобы защитить нулевую страницу (NULL). Любое обращение к 0x0 вызывает исключение, и я сразу вижу, где в коде баг, да и если у пользователя приложение обратиться к 0x0 ОС сразу же вызовет RSoD.

Demand Paging (Ленивая загрузка)

Память выше 64 МБ помечается как Not Present. Когда программа пытается туда обратиться, процессор кидает Page Fault (#PF).

Ядро ловит это, выделяет физическую страницу и возвращает управление. Программа даже не замечает подвоха.

// Обработчик Page Fault
pub fn handle_page_fault(error_code: u32, fault_addr: u32) void {
    // Если адрес в зоне Demand Paging (>64MB)
    if (fault_addr >= 0x4000000) {
        const pde_index = fault_addr >> 22;
        const pde = &page_directory[pde_index];
        
        // Просто ставим бит Present
        pde.* |= 0x1; // Present bit
        
        // Сбрасываем TLB
        asm volatile ("invlpg (%[addr])" : : [addr] "r" (fault_addr));
        return; // Продолжаем выполнение
    }
    
    // Иначе — это реальная ошибка, вызываем RSOD
    rsod("Page Fault", error_code, fault_addr);
}
Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами - 5

Red Screen of Death (RSOD)

Синего экрана у нас нет. У нас есть Красный.

Если ядро ловит Triple Fault или критическое исключение, драйвер VGA игнорирует все блокировки, заливает экран красным и показывает дампы регистров (EAX, EIP). Это не раз спасало(ОС и нервы) при отладке драйвера FAT и Paging-а.

RSoD после деления на ноль

RSoD после деления на ноль

Файловая система: Честный FAT32

До v0.20 у нас была базовая поддержка FAT12/16 (для дискет и маленьких дисков) и RAM-диск. В этом релизе я добавил полноценный драйвер FAT32.

Что это дает?

  • Поддержку дисков любого объема (проверял до 32 ГБ).

  • Длинные имена файлов (LFN): Вместо MYFILE~1.TXT система видит MyHabrFile.txt. Пришлось писать парсер, который склеивает имя из нескольких записей в каталоге.

  • Утилиту mkfs-fat32: Можно отформатировать диск прямо из OS.

Код драйвера полностью на Zig, структура BPB и записи каталогов мапятся прямо на буфер сектора.

// Чтение FAT32 BPB (Boot Parameter Block)
pub fn read_bpb(drive: ata.Drive) ?BPB32 {
    var sector: [512]u8 align(4) = undefined;
    ata.read_sector(drive, 0, &sector);
    
    // Прямое приведение типов — zero-copy!
    const bpb = @as(*BPB32, @ptrCast(@alignCast(&sector))).*;
    
    // Определяем тип FAT по количеству кластеров
    const total_clusters = (bpb.total_sectors_32 - reserved) / bpb.sectors_per_cluster;
    
    if (total_clusters >= 65525) {
        return bpb; // Это FAT32
    }
    return null;
}

// Парсинг Long File Name (LFN)
fn extract_lfn_part(lfn_entry: *const DirEntry, out: []u8) usize {
    var pos: usize = 0;
    // LFN хранится в UTF-16, склеиваем из 3 частей
    for (lfn_entry.name1) |c| {
        if (c == 0 or c == 0xFFFF) break;
        out[pos] = @intCast(c & 0xFF);
        pos += 1;
    }
    // ... аналогично для name2 и name3
    return pos;
}
Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами - 7

Shell, Nova Scripting и Editor

Мне было скучно просто запускать утилиты, поэтому я начал с создания Nova — встроенного языка сценариев.

Он похож на Python/JS:

print("Hello Habr!");
set int a = 10;
if (a > 5) {
    print("Greater than 5");
}
print("Number is " + a + "!");
Пишем свою OS на Zig: Первый релиз с многоядерностью, FAT32 и скриптами - 8
Выполнение скрипта на Nova

Выполнение скрипта на Nova

Позже, когда появилась поддержка FAT12/16, я написал полноценный текстовый редактор (edit).

Это не просто cat > file, а интерактивный инструмент с навигацией и поддержкой прокрутки больших файлов.

Скрипты могут управлять файлами, вызывать системные команды и даже работать с пользователем. Сам Shell поддерживает пайпы (|), редиректы (>), историю команд и автодополнение.

Работа с файлами

Работа с файлами

Что в итоге?

NovumOS v0.20 — это уже не просто "Hello World" загрузчик.

Это система, которая:

  • Грузится с реального железа (USB/HDD).

  • Использует все ядра CPU.

  • Позволяет писать скрипты и работать с файлами.

  • Имеет открытый код (Apache 2.0).

  • Работает даже на телефоне! (через эмуляторы типа Limbo x86).

В планах: графический интерфейс (VESA), вытесняющая многозадачность и сетевой стек.

Исходный код на GitHub

Буду рад любым вопросам в комментариях: про архитектуру, про Zig или про то, как отлаживать Triple Fault в 3 часа ночи.

Автор: MinecAnton209

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js