- PVSM.RU - https://www.pvsm.ru -
Привет! 👋
Меня зовут Антон, и я хочу поделиться историей создания своей собственной операционной системы.
Это моя первая статья и первый серьезный релиз системы (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, и как подружить ассемблерные трамплины с безопасным языком программирования — добро пожаловать.
Обычно 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);
}
}
Самым сложным вызовом стала реализация Symmetric Multiprocessing (SMP). При старте компьютера BIOS запускает только одно ядро (BSP). Остальные (AP) спят. Чтобы их разбудить, нужно отправить специальную последовательность прерываний (INIT-SIPI-SIPI) через контроллер APIC.
Ядра просыпаются в 16-битном Real Mode. Они ничего не знают о защищенном режиме.
Для них пришлось писать Трамплин (Trampoline) — маленький кусок кода на ASM, который:
Загружается по адресу 0x8000 (куда могут дотянуться 16-битные ядра).
Переключает процессор в Protected Mode.
Настраивает стек (свой для каждого ядра).
Прыгает в основное ядро на 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);
// ... добавляем задачу в очередь
}
Ещё одной интересной задачей стало управление памятью.
Вместо классических 4КБ страниц я решил использовать Huge Pages (4 МБ).
Это позволяет одним махом замапить ядро, стек и видеопамять, снижая нагрузку на TLB процессора.
Однако, первые 4 МБ я оставил разбитыми по 4 КБ. Но зачем? Чтобы защитить нулевую страницу (NULL). Любое обращение к 0x0 вызывает исключение, и я сразу вижу, где в коде баг, да и если у пользователя приложение обратиться к 0x0 ОС сразу же вызовет RSoD.
Память выше 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);
}
Синего экрана у нас нет. У нас есть Красный.
Если ядро ловит Triple Fault или критическое исключение, драйвер VGA игнорирует все блокировки, заливает экран красным и показывает дампы регистров (EAX, EIP). Это не раз спасало(ОС и нервы) при отладке драйвера FAT и Paging-а.
До 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, §or);
// Прямое приведение типов — zero-copy!
const bpb = @as(*BPB32, @ptrCast(@alignCast(§or))).*;
// Определяем тип 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;
}
Мне было скучно просто запускать утилиты, поэтому я начал с создания Nova — встроенного языка сценариев.
Он похож на Python/JS:
print("Hello Habr!");
set int a = 10;
if (a > 5) {
print("Greater than 5");
}
print("Number is " + a + "!");
Позже, когда появилась поддержка FAT12/16, я написал полноценный текстовый редактор (edit).
Это не просто cat > file, а интерактивный инструмент с навигацией и поддержкой прокрутки больших файлов.
Скрипты могут управлять файлами, вызывать системные команды и даже работать с пользователем. Сам Shell поддерживает пайпы (|), редиректы (>), историю команд и автодополнение.
NovumOS v0.20 — это уже не просто "Hello World" загрузчик.
Это система, которая:
Грузится с реального железа (USB/HDD).
Использует все ядра CPU.
Позволяет писать скрипты и работать с файлами.
Имеет открытый код (Apache 2.0).
Работает даже на телефоне! (через эмуляторы типа Limbo x86).
В планах: графический интерфейс (VESA), вытесняющая многозадачность и сетевой стек.
Буду рад любым вопросам в комментариях: про архитектуру, про Zig или про то, как отлаживать Triple Fault в 3 часа ночи.
Автор: MinecAnton209
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/x86/444556
Ссылки в тексте:
[1] Исходный код на GitHub: https://github.com/MinecAnton209/NovumOS
[2] Источник: https://habr.com/ru/articles/995136/?utm_source=habrahabr&utm_medium=rss&utm_campaign=995136
Нажмите здесь для печати.