Привет, Хабр! ?

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

Это моя первая статья и первый серьезный релиз системы (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);
    }
}

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);
    // ... добавляем задачу в очередь
}
Утилита 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);
}

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;
}

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 + "!");
Выполнение скрипта на 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 часа ночи.

Комментарии (18)


  1. Kahelman
    11.02.2026 08:43

    Класс, только вчера вечером про Zig читал. Было не ясно- он живой в принципе или как, а тут уже товарищи на нем новую ОС написали. :)

    Успехов.

    Поделитесь опытом, как вы на Zig подсели и что там вообще происходит


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Спасибо за пожелания. Рад, что статья попалась вам как раз вовремя.

      Zig сейчас один из самых динамично развивающихся системных языков. На нем уже написаны такие серьезные штуки, как рантайм Bun или бд TigerBeetle.

      Почему я «подсел» на него при разработке ядра:

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

      • В Zig нет скрытых аллокаций. Если функция что-то выделяет — она просит аллокатор. В Ring 0 — ты контролируешь каждый байт.

      • Интероп с C/ASM: Zig умеет импортировать C-хедеры напрямую, а работа с ассемблерными вставками гораздо приятнее, чем в классическом GCC.

      После C/C++ Zig ощущается как «Си, который наконец-то починили». Нет нужды в заголовочных файлах, есть нормальные слайсы и отличная система обработки ошибок.


    1. domix32
      11.02.2026 08:43

      Да жив и развивается. Но пока всё ещё в бете. Не так давно IO обновили, добавив IOContext из которого уже можно собирать поллинг всех цветов фломастеров и соотвественно асинхронные экзекуторы. А штуки типа Bun и Tiger Beetle DB поддерживают спрос в проде.


    1. Annsky
      11.02.2026 08:43

      Не знаю живой он или нет, но кросскомпиляция из коробки за 200мб под любую архитектуру и ОС это киллер-фича лично для меня.


  1. tooshcan4ik
    11.02.2026 08:43

    Красавчик! Звезду тебе на гитхабе поставил!


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Спасибо большое за поддержку! Очень приятно, что проект нашел отклик.


  1. MasterMentor
    11.02.2026 08:43

    Статья/карма/подписка: +/+/+.

    PS неплохой пэт проект для самообразования!


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Спасибо за поддержку и подписку! Именно для самообразования проект и задумывался. Рад, что формат статьи зашел.


  1. hurdos
    11.02.2026 08:43

    Впечатляет! Starred!

    Жаль только перспективы туманны, хотя наверное это в чистом виде пет-проект.

    Но кто знает, возможно станет на уровне linux когда-нибудь.


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Спасибо за звезду на GitHub! На данный момент это действительно пет-проект. Но кто знает, во что это может вырасти с поддержкой сообщества. В планах как раз создание стабильной базы, на которой можно будет запускать изолированные пользовательские приложения.


  1. Number_One_890
    11.02.2026 08:43

    Очень хорошая статья, для меня ценная.


  1. Supme
    11.02.2026 08:43

    time - Show current date and time

    Жаль что не date это делает, а time засекает время выполнения команды. Уже привычка в *nix у всех.


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Справедливое замечание. В v0.24 обязательно добавлю алиас date для вывода времени, а логику time переделаю под замер длительности выполнения команд.


  1. maxscitech
    11.02.2026 08:43

    Очень круто. И спасибо что напомнил про Zig.


  1. eoanermine
    11.02.2026 08:43

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

    Нет скрытых аллокаций

    Comptime

    Взаимодействие ASM

    А как это помогает избежать ошибок при работе с памятью? Только благодаря наличию в языке defer?


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Defer— это отличный инструмент для предотвращения утечек, но безопасность Zig в ядре строится на более фундаментальных вещах:

      1. Слайсы: В C мы передаем char* и надеемся, что не вылетим за пределы. В Zig []u8 — это структура из указателя и длины. Компилятор (и рантайм-чеки в дебаге) просто не дадут тебе обратиться к data[limit + 1].

      2. Optional Types (?T): В Zig нельзя просто так разыменовать указатель, который может быть null. Язык заставляет тебя сначала проверить его (if (ptr) |p| ...). Это убивает целый класс ошибок «NULL Pointer Dereference» еще на этапе написания кода.

      3. Явные аллокаторы: В ядре нет «магической» кучи. Каждая функция, которой нужна память, принимает аллокатор как аргумент.

      4. Error Unions: Ошибки — это часть системы типов. Ты не можешь проигнорировать результат read_sector, компилятор заставит тебя обработать возможный error.DiskFailure.

      Так что нет, это не только defer. Это комбинация строгого контроля типов и отсутствия неявного поведения.


  1. ykomi
    11.02.2026 08:43

    Не боишься того, что в будущем возможно придётся переписывать тонну кода на "новый" Zig? Он же не в релизе ещё, а значит оттуда могут спокойно выпилить что угодно. Даже я с этим столкнулся когда его начал изучать на маленьких проектах, а ОС вообще большой проект


    1. MinecAnton209 Автор
      11.02.2026 08:43

      Риск есть, и я с ним уже сталкивался. Но для OSDev это скорее плюс, чем минус:

      1. Язык развивается в сторону упрощения: Каждое крупное изменение в Zig обычно выпиливает какой-то магический кусок из C или делает синтаксис более логичным. Обновлять код под новые версии помогает лучше понять, как работают внутренности компилятора.

      2. Малый объем синтаксиса: Zig очень компактный. Даже если завтра изменят способ объявления export fn, пройтись по 11к строкам и поправить это — задача на один вечер, зато профит от использования comptime и отсутствия legacy-мусора C перекрывает эти затраты.

      3. Архитектура важнее синтаксиса: Основная сложность моей ОС — это логика SMP, работа с таблицами страниц и ACPI. Это всё не изменится от того, что в Zig поменяют название ключевого слова.