Приветствую, Хабравчане!

В этой серии статей мы будем писать монолитное ядро на C++17 с упором на чистую архитектуру, тестируемый код и минимальное использование ассемблера. Меня всегда привлекала разработка операционных систем, но на пути к ней часто стояли барьеры: тонны ассемблерных вставок, макросы и низкоуровневые хаки. За последние 10 лет набравшись опыта в C++, я решил вернуться к этой теме с новым подходом — максимально использовать современные возможности языка для создания понятного, поддерживаемого кода ядра.

В этой части мы создадим Hardware Abstraction Layer (HAL) для консольного вывода, реализуем две версии ядра (для отладки на хосте и для "голого железа") и успешно запустим наше ядро в QEMU.

Почему C++17 и что мы будем использовать

Мы сознательно ограничим некоторые возможности языка для работы в среде ядра:

  • Без исключений — очень дорого и требует проф ядра.

  • Без RTTI — просто дорого.

  • С ограниченной стандартной библиотеки на первом этапе -

  • Стандартной библиотеке С++ и STL быть. Для раннего этапа буду использовать контейнеры, без динамической памяти, к примеру std::array. Когда будет готова работа с динамической памятью в ядре, я глобально для ядра переопределю, new и delete. И в самом ядре, можно будет использовать STL без ограничений, std::vector, std::unordered_map и т.д

    Минимум костылей и велосипедостроения.

  • Своя реализация new/delete — на первых порах это будут заглушки, позже реализуем кучу

Но при этом активно используем:

  • Классы и наследование для абстракций

  • Виртуальные методы для полиморфизма

  • Шаблоны (в будущих частях)

  • constexpr и noexcept

  • Placement new для работы с сырой памятью

    Установка инструментария

    Для работы нам понадобится кросс-компилятор и эмулятор. Я использую WSL/Ubuntu, но подойдёт любой Linux-дистрибутив.

Лично я использую WSL.

# Обновляем пакеты
sudo apt update && sudo apt upgrade -y

# Ставим инструменты для сборки
sudo apt install build-essential make git nano -y

# Устанавливаем эмулятор QEMU
sudo apt install qemu-system-x86 -y

# Устанавливаем кросс-компилятор для x86
sudo apt install gcc-i686-linux-gnu g++-i686-linux-gnu binutils-i686-linux-gnu -y

После портянок в терминале, все будет установлено. Переходим к разработке.

Я предлагаю маленькими шагами идти по пути разработки ОС. Первый шаг, это просто вывести строку "Running SimpleOS", да так я назвал проект.

Концепция: Hardware Abstraction Layer (HAL)

HAL — это слой между физическим железом и ядром ОС. Его цель — скрыть аппаратные особенности за унифицированными интерфейсами. Это даёт нам несколько преимуществ:

  1. Переносимость — ядро не зависит от конкретного железа

  2. Тестируемость — можно создавать mock-реализации для отладки

  3. Чистая архитектура — разделение ответственности

Начнём с самого простого — абстракции для консольного вывода.

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

Я предлагаю начать с разработки абстрактного HAL, а так же для демонстрации концепции, мы напишем консольный вывод в обычную консоль Windows, Linux и для голого железа x86.

Поехали.


#pragma once

#include <new>
#include <cstdint>

namespace HAL
{
	class IConsole
	{
	public:
		virtual ~IConsole() = default;
		virtual void Clear() = 0;
		virtual void Write(char c) = 0;
		virtual void Write(const char* src) = 0;
	};
}

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

Теперь перейдем к реализации для использования с iostream


#include <iostream>
#include <SimpleOS/Console.hpp>

using namespace HAL;

Console::Console()
{
}

Console::~Console()
{
}

void Console::Clear()
{
}

void Console::Write(char c)
{
	std::cout << c;
}

void Console::Write(const char* src)
{
	std::cout << src;
}

Так же создадим ядро нашей ос.


#include <SimpleOS/Kernel.hpp>

using namespace HAL;

Kernel::Kernel() :
	_console(nullptr)
{
	_console = new (_consoleBuffer) Console();
}

Kernel::~Kernel()
{
	if (_console)
	{
		_console->~IConsole();
	}
}

void Kernel::Run()
{
	while (true)
	{
		_console->Write("Running SimpleOS\n");
	}
}

И будем вызывать из main


#include <SimpleOS/Kernel.hpp>

int main()
{
    Kernel kernel;
    kernel.Run();

    return 0;
}

Уверен, у вас возникает вопрос и где ОС? Вывод в консоль... Что происходит?

А в этом и есть преимущество HAL, мы можем написать абстрактные классы с единым интерфейсом, но для упрощения отладки или тестировании логики замокать их. Предоставив другу реализацию. Когда мы дойдем до графики, то добавим просто SDL и всю логику вывода и рисования графики будем отлаживать на ней, а уже потом просто реализуем железную реализацию. Ну клево же?

Для нашего ядра создаем отдельный файл X86Main.cpp


#include <SimpleOS/Kernel.hpp>

extern "C" void KernelMain()
{
    Kernel kernel;
    kernel.Run();
}

Код абсолютно тот же, только его запуск происходит из другого файла. KernelMain вызывает загрузчик.

Вот наша реализация железной консоли


#include <SimpleOS/Console.hpp>

using namespace HAL;

Console::Console() :
    _cursorX(0),
    _cursorY(0),
    _buffer((uint16_t*)0xB8000)
{
}

Console::~Console()
{
}

void Console::Clear()
{
    for (size_t i = 0; i < 80 * 25; i++) 
    {
        Write(' ', i % 80, i / 80);
    }

    _cursorX = 0;
    _cursorY = 0;
}

void Console::Write(char c)
{
    if (c == '\n') 
    {
        _cursorX = 0;
        _cursorY++;
    }
    else 
    {
        Write(c, _cursorX, _cursorY);
        _cursorX++;

        if (_cursorX >= 80)
        {
            _cursorX = 0;
            _cursorY++;
        }
    }

    if (_cursorY >= 25)
    {
        _cursorY = 24;
    }
}

void Console::Write(const char* src)
{
    while (*src)
    {
        Write(*src++);
    }
}

void Console::Write(char c, uint8_t x, uint8_t y)
{
    _buffer[y * 80 + x] = (0x0F << 8) | c;
}

Это реализация драйвера текстовой консоли для VGA-режима 80×25. Код управляет выводом символов прямо в видеопамять компьютера, которая находится по фиксированному адресу 0xB8000.

Конструктор настраивает начальную позицию курсора в левом верхнем углу экрана (координаты 0,0). Метод Clear() заполняет все 80 столбцов и 25 строк экрана пробелами, создавая эффект "чистого экрана".

Основной метод Write() обрабатывает печать символов: обычные символы выводятся в текущую позицию курсора, а при получении символа перевода строки (\n) курсор перемещается на следующую строку.

Наш код выполняется на голом железе и выводит символы на экран. Магия? Нет абстракция.

Заголовочный файл ядра выглядит так:

#pragma once

#include <SimpleOS/Console.hpp>

class Kernel
{
public:
	Kernel();
	~Kernel();
	void Run();
private:
	alignas(HAL::Console) uint8_t _consoleBuffer[sizeof(HAL::Console)];
	HAL::IConsole* _console;
};

Здесь мы сознательно избегаем обычного new и выделяем память вручную _consoleBuffer это просто массив байтов нужного размера. Директива alignas гарантирует, что этот буфер будет расположен в памяти с правильным выравниванием для класса HAL::Console. Позже, в конструкторе ядра, мы создадим объект консоли прямо в этом буфере с помощью placement new. Такой подход позволяет нам контролировать размещение объектов в памяти, что критически важно в среде ядра, где менеджер памяти ещё не готов.

Kernel::Kernel() :
	_console(nullptr)
{
	_console = new (_consoleBuffer) Console();
}

Прямо в конструкторе ядра мы создаём экземпляр консоли. На данном этапе это допустимое упрощение, но с архитектурной точки зрения здесь есть проблема: конструкторы не должны выполнять сложную инициализацию с возможностью сбоя.

Дело в том, что в конструкторах невозможно корректно обработать ошибки без механизма исключений, а исключения в ядре нет и на данном этапе не возможно встроить в ядро.

Более правильный подход — разделить создание объекта и его инициализацию. В конструкторе следует лишь обнулить память или установить POD-типам значения по умолчанию, а всю сложную логику с возможными ошибками вынести в отдельный метод, например Initialize() или Setup(). Тогда мы сможем явно проверить успешность инициализации и корректно отреагировать на сбой, прежде чем продолжить работу ядра.

Kernel::~Kernel()
{
	if (_console)
	{
		_console->~IConsole();
	}
}

В деструкторе ядра вызываем деструктор для консоли. Деструктор сейчас у нее пустой.

void Kernel::Run()
{
	while (true)
	{
		_console->Write("Running SimpleOS\n");
	}
}

Это наш главный цикл ядра, просто бесконечно выводим строку.

Конечно обошлось не без но:


#pragma once

#include <new>
#include <cstddef>
#include <cstdint>

[[nodiscard]] inline void* operator new(size_t size) noexcept
{
    (void)size;
    asm volatile("cli; hlt");
    __builtin_unreachable();
}


inline void operator delete(void* ptr) noexcept
{
    (void)ptr;
}

inline void operator delete(void* ptr, size_t size) noexcept
{
    (void)ptr;
    (void)size;
}

Это временная заглушка для операторов работы с памятью. Поскольку в нашей ОС пока нет полноценного менеджера памяти, мы переопределяем стандартные new и delete. Если код случайно попытается выделить память через new, система безопасно остановится — это лучше, чем получить непредсказуемые ошибки.

Теперь самое интересное — сборка и запуск. Честно признаюсь: я начал с попыток настроить сборку через CMake, но после пары часов борьбы с кросс-компиляцией и скриптами линковки понял, что для первых шагов старый добрый Makefile будет проще и нагляднее. Иногда простые инструменты лучше справляются со специфичными задачами системного программирования.

Скрытый текст

TARGET = i686-linux-gnu
CC = $(TARGET)-gcc
CXX = $(TARGET)-g++
AS = $(TARGET)-as
LD = $(TARGET)-ld

SRCDIR        = source
INCDIR        = include
X86DIR        = source/x86
SIMPLEOSDIR   = source/SimpleOS
X86SIMPLEOSDIR= source/x86/SimpleOS

CFLAGS = -ffreestanding -O2 -Wall -Wextra
CXXFLAGS = -ffreestanding -O2 -Wall -Wextra -fno-exceptions -fno-rtti
CXXFLAGS += -I$(INCDIR) -I$(SRCDIR) -I$(X86DIR) -I$(X86SIMPLEOSDIR) # Добавляем путь к ExtNew.hpp

ASFLAGS = --32

OBJS = boot.o X86Main.o Kernel.o Console_x86.o

KERNEL = myos.bin

.PHONY: all clean run

all: $(KERNEL)

$(KERNEL): $(OBJS)
	$(LD) $(OBJS) -T $(X86DIR)/linker.ld -o $@

boot.o: $(X86DIR)/boot.asm
	$(AS) $(ASFLAGS) $< -o $@

X86Main.o: X86Main.cpp $(SIMPLEOSDIR)/Kernel.hpp $(X86SIMPLEOSDIR)/ExtNew.hpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

Kernel.o: $(SIMPLEOSDIR)/Kernel.cpp $(SIMPLEOSDIR)/Kernel.hpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

Console_x86.o: $(X86SIMPLEOSDIR)/Console.cpp $(X86SIMPLEOSDIR)/Console.hpp $(X86SIMPLEOSDIR)/ExtNew.hpp
	$(CXX) $(CXXFLAGS) -c $< -o $@


clean:
	rm -f $(OBJS) $(KERNEL) *.o

run: $(KERNEL)
	qemu-system-i386 -kernel $(KERNEL)

Таким многословным мэйк файлом собирается наш проект.

Естественно без асма никуда, нам нужно написать свой загрузчик.

.section .multiboot
.align 4
.long 0x1BADB002
.long 0x00
.long - (0x1BADB002 + 0x00)

.section .text
.global _start

_start:

    call KernelMain

halt:
    hlt
    jmp halt

В двух словах этот загрузчик вызывает нашу функцию KernelMain и позволяет загрузиться в qemu - эмулятор.

Для линковки есть другой файл

ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
    . = 0x100000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss)  }
}

Он связывает наш код и добавляет информацию с какого адреса будем грузиться.

Сборка стандартная.

make

Есть немного ворнингов, но работает:)

i686-linux-gnu-as --32 source/x86/boot.asm -o boot.o
i686-linux-gnu-g++ -ffreestanding -O2 -Wall -Wextra -fno-exceptions -fno-rtti -Iinclude -Isource -Isource/x86 -Isource/x86/SimpleOS -c X86Main.cpp -o X86Main.o
i686-linux-gnu-g++ -ffreestanding -O2 -Wall -Wextra -fno-exceptions -fno-rtti -Iinclude -Isource -Isource/x86 -Isource/x86/SimpleOS -c source/SimpleOS/Kernel.cpp -o Kernel.o
i686-linux-gnu-g++ -ffreestanding -O2 -Wall -Wextra -fno-exceptions -fno-rtti -Iinclude -Isource -Isource/x86 -Isource/x86/SimpleOS -c source/x86/SimpleOS/Console.cpp -o Console_x86.o
i686-linux-gnu-ld boot.o X86Main.o Kernel.o Console_x86.o -T source/x86/linker.ld -o myos.bin
i686-linux-gnu-ld: warning: boot.o: missing .note.GNU-stack section implies executable stack
i686-linux-gnu-ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker
i686-linux-gnu-ld: warning: myos.bin has a LOAD segment with RWX permissions

Последний шаг, вызываем qemu с загрузкой нашего ядра.

make run

И брюки превращаются...

Архитектурные решения и почему они важны

  1. Использование placement new — мы не можем использовать обычный new до инициализации кучи, поэтому размещаем объекты в предварительно выделенной памяти.

  2. Виртуальные методы в HAL — позволяют легко подменять реализации. Хотите выводить в UART вместо VGA? Просто создайте новый класс.

  3. Отдельные точки входа — main() для хоста и KernelMain() для bare-metal позволяют иметь одну кодовую базу для двух сред.

  4. Заглушки для new/delete — безопасная обработка ситуаций, когда куча ещё не готова.

Начало положено, наш код выполняется на голом железе.

Задавайте вопросы в комментариях! Я сам только начинаю погружаться в разработку ОС, поэтому мой опыт всего на шаг впереди вашего.

Конечно, мы не создадим следующую Windows или Linux, но это и не цель. Гораздо ценнее то, что мы своими руками разберёмся в основах, поймём, как устроены операционные системы изнутри, и получим уникальный опыт работы с настоящим железом. А там, кто знает, куда нас заведёт этот интерес...

Ссылка на проект: https://github.com/JordanCpp/SimpleOS

Буду рад, советам, критике и предложениям. Расскажите о своем опыте.

Обновил информацию в части использования стандартной библиотеки С++.

Стандартной библиотеке С++ и STL быть. Для раннего этапа буду использовать контейнеры, без динамической памяти, к примеру std::array. Когда будет готова работа с динамической памятью в ядре, я глобально для ядра переопределю, new и delete. И в самом ядре, можно будет использовать STL без ограничений, std::vector, std::unordered_map и т.д

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


  1. vi_is_raven
    01.12.2025 15:08

    Сначала дико орнул, когда увидел всю мощь сего ядра в виндовом окне цмд, но потом, вчитываясь, понял, что это просто гайд на нормальное использование ООП в специфической области. неплохо!


    1. JordanCpp Автор
      01.12.2025 15:08

      В том числе, вы правы. Возможно даже в первую очередь меня интересует не функционал, а как сделать код простым, понятным, элегантным и современным в контексте С++.


  1. cd20bn
    01.12.2025 15:08

    Автор, по-моему, на выходе вы получили просто программу, печатающую "я SimpleOS" без использования страндартной библиотеки - и требующую для запуска и работы наличия ОС Ubuntu и эмулятора QEMU как минимум :D

    Назвать это простой ОС с "доступом к "железу" - весьма странно. Любая другая программа user mode имеет ровно такой же "доступ", разве нет?

    Для ОС логично было бы начинать со своего собственного системного загрузчика?

    А называть методы и классы Kernel и HAL - кажется несколько преждевременным.


    1. JordanCpp Автор
      01.12.2025 15:08

      Все так. Но начинать с загрузчика не так интересно, нет той обратной связи, слишком много ассемблера для одной статьи в голову:)


  1. Slavik2025
    01.12.2025 15:08

    В принципе неплохо, но не соответствует требованиям програм для искусственного интеллекта, когда вначале программы для ИИ идёт описание структурных элементов интеллекта, затем набор этих элементов, и взаимосвязи между элементами, описание более глобальных элементов, затем соединения этих глобальных элементов, если в этом коде есть описание вычислителей с системой команд вычислителя, например x86, то тогда в такой код можно и впихнуть затем вашу реализацию ОС. А если описание команд x86 сложно для понимания - можно свою систему команд сварганить. Глвное чтобы были интерпритаторы такого кода в искине, которые настраивают транзисторы и прочие пппаратные междусоединения.


  1. includedlibrary
    01.12.2025 15:08

    Интересно будет почитать, если не забросите! Я несколько месяцев назад тоже начал свою ОС делать, но только на C, а не на C++. Столкнулся с довольно интересным моментом, что практически все статьи в интернете расчитывают на запуск через BIOS, а не UEFI, а в long mode нельзя просто так брать и писать в VGA память текст, то есть для простейшего вывода надо писать свой рендер битовых шрифтов. Я пока ограничился выводом через UART.


    1. JordanCpp Автор
      01.12.2025 15:08

      Пока у меня знаний мало, набиваю руку. Так как эти сложности решены, буду переимпользовать код из других проектов. Естественно сначала разбираюсь, переписываю на С++, внедряю в проект.


  1. Jijiki
    01.12.2025 15:08

    класс, успехов вам. а я наконец-то написал С со своим VM (парсер-лексер - ast - bytecode(highlevel asm emulator) все на С) - это невероятно


    1. JordanCpp Автор
      01.12.2025 15:08

      Спасибо.


    1. Kealon
      01.12.2025 15:08

      А codesite для c проходит?


      1. Jijiki
        01.12.2025 15:08

        не знаю что это такое, там всё проходит по выбранному синтаксису(надо выбрать синтаксис, потомучто его придётся парсить в дерево, дерево поможет понять как парсить такое ну и прочее, вообще получается можно спокойно на форте писать с таким раскладом, ну там на любителя, может быть так, что язык нравится, но компилятор не нравится ) ) через парсер в байтовую машину, вообще это паттерн, как я понял там нет ничего не обычного, единственное это аст и нюансы языка, тоесть программа выполняется в песочнице, не гонится код в маш код сразу, вообще это мой первый компилятор, я не силен в машинных кодах), надо вспоминать ассемблер тут уже стало понятно зачем всё это )

        я сделал функцию желаемого языка и пока остановился на калькуляторе, потомучто там уже понятно что делать, щас надо тащить ассемблер

        вообще в изучении языка помогает писать компилятор кстати, я чутка больше понял, советую


  1. Notevil
    01.12.2025 15:08

    Почему C++17 и что мы будем использовать

    Таки почему 17, а не 23?


    1. JordanCpp Автор
      01.12.2025 15:08

      Старт на С++ 17. Но всегда можно будет повысить версию.

      Сорри, я вас нечайно минуснул, пальцем промахнулся. Отвечал с телефона.


  1. domix32
    01.12.2025 15:08

    вроде обещали constexpr, а по всему коду магические значений разбросаны. всякие 0xB8000 и 80/25 размазаны по всему коду. Для читателей этой статьи-то понятно конечно что одно это указатель на VGA буффер и соотвественно размеры, но дял читателя кода это ни разу не очевидно.

    noexcept кстати тоже ни разу не появился нигде кроме new/delete.

    ASFLAGS = --32

    я кстати правильно понимаю ,что 64-битная архитектура пока не поддерживается совсем


    1. JordanCpp Автор
      01.12.2025 15:08

      Все так я уже думал, насчет следующей статьи, что кратко опишу рефакторинг кода, constexpr, nonoexcept добавлю и перейду к реализации прерываний. Спасибо, что обратили внимание.

      Пока 32 бит, но со временем добавлю и 64 бит. Просто сейчас я пока не знаю как сделать. Но обязательно добавлю.


  1. Zara6502
    01.12.2025 15:08

    эдак вы на UE5 что-то сделаете, обзовёте "ОС".

    HAL - это не заполнение буфера экранной памяти ))))

    а консоль - вообще необязательная часть ядра, это скорее один из способов вывода (один из многих)


    1. JordanCpp Автор
      01.12.2025 15:08

      С тем же успехом можно назвать и первые уроки, по написанию загрузчика и проверки загрузки ОС с одной функцией start. Что все это не относится к разработке ОС.

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

      Вот и вы и сами говорите один из способов вывода, вот в статье показано как абстрагировать один из способов вывода.

      Я не спорю что стандартные статьи на эту тему больше, полно кода, я же двигаюсь небольшими шагами. У меня есть работа и статьи пишу в свободное время. Узнал, реализовал, написал статью.

      Сам Линус говорил, что его первая ос была 11 строк кода:))


      1. Zara6502
        01.12.2025 15:08

        Вот и вы и сами говорите один из способов вывода

        и? это к ядру ОС как относится? вы можете и без наличия ОС выводить информацию куда угодно. INT 13H например (если это часть БИОС VGA).

        например command.com это уже надстройка над ОС, а не HAL.


        1. includedlibrary
          01.12.2025 15:08

          В защиту могу сказать, что это только первый этап. Я тоже начинал написание ядра с вывода в UART, просто потому что иногда так проще отлаживать, чем через отладчик.


        1. JordanCpp Автор
          01.12.2025 15:08

          Это первый этап. Самое начало. Ядро же выводит информацию, что значит как относится? Самым прямым образом.

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


          1. zanzack
            01.12.2025 15:08

            Все этапы: прерывания, память и тд. включая линию A20 уже описаны "программистом из Латвии" в 2012 году https://habr.com/ru/articles/160427/

            Чем же всё закончилось? Науке это неизвестно, но его сайт пока жив!
            Внимательно следим.

            Кстати, самый полезный комментарий звучит так:

            Надо начинать всегда с эмулятора терминала, тогда точно взлетит.


  1. cdriper
    01.12.2025 15:08

    alignas(HAL::Console) uint8_t _consoleBuffer[sizeof(HAL::Console)];

    перемудрили на ровном месте

    зачем, если с таким кодом смена типа, который реализует IConsole, все равно требует переписывание нескольких мест в том же классе?

    общая идея runtime полиморфизма в том, чтобы не перекомпилируя существующий код, заставить его работать с любым набором совместимых типов


    1. cdriper
      01.12.2025 15:08

      • если собираетесь использовать std, то брали бы сразу unique_ptr (с кастомным делитером)

      • начинать лучше с вводной статьи, где расскажите что вы вообще собираетесь делать, какая архитектура и по каким конкретным шагам вы собираетесь ее реализовывать


    1. JordanCpp Автор
      01.12.2025 15:08

      Вы правы, думаю следует добавить функции для создания, которые уже возвращают инициализированный объект. В конструкторе nullptr, в методе Initialize уже проверяем и вызываем _console = Hal::CreateConsole();

      Тогда в приват части простото бъявленпе указателя. Вы в частности и об этом говорили?


      1. cdriper
        01.12.2025 15:08

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

        • а нужно ли вообще тут абстрагирование от типа?

        • можно задать тип консоли через шаблон


        1. JordanCpp Автор
          01.12.2025 15:08

          Задача абстрагироваться от железа, простым слоем. Максимально не меняя ядро, обеспечить перенос на другие архитектуры, изменяя только HAL уровень.

          По мере разработки я уверен, придет понимание как правильно абстрагироваться, где применить абстрактный класс, а где нужен шаблон и т.д


          1. cdriper
            01.12.2025 15:08

            тогда у тебя вариативность в compile time
            значит в console.h описываешь интерфейс класса
            дальше идут реализации console_x86.cpp, console_ARM.cpp и так далее
            полиморфизм тут вообще не нужен


    1. JordanCpp Автор
      01.12.2025 15:08

      Нет, названия для хоста и x86 одинаковы, я подменяю только пути инклюдов и исходных файлов при сборке. И оно собирается без горы ifdef


  1. titbit
    01.12.2025 15:08

    Пишем ядро ОС на modern C++ без макросов

    Так а почему именно C++ да еще без макросов? Про это ни слова, особенно в контексте именно ОС, а не обычных программ. В статье написано какими плюшками языка можно пользоваться, но почему выбран именно C++ например?

    Архитектурные решения и почему они важны

    Это же не архитектурные решения, а просто технические моменты. А вот про архитектуру ОС у вас почти ни слова, кроме абстрактной фразы про "будет писать монолитное ядро с HAL". Хотелось бы подробностей. Почему монолитное ядро, например?

    мы своими руками разберёмся в основах, поймём, как устроены операционные системы изнутри

    ОС это ведь не столько работа с "железом" (которое очень разнообразно и его стараются отделить в подсистему условных драйверов), сколько точный менеджмент ресурсов: процессоров, памяти, времени, привилегий и т.д. Вот с этого стоило бы начать на мой взгялд, и главное эти алгоритмы гораздо проще тестировать в обычной среде и не надо возиться с загрузчиками и QEMU.


    1. JordanCpp Автор
      01.12.2025 15:08

      Я знаю С++ и люблю его использовать. И как раз его уровень абстракция/оптимизация позволяет его использовать в ядре. Это больше мой личный выбор.

      Монолитное ядро проще и его вполне хватит что бы понять основы, концепции, возможность пощупать железо. Практическая целесообразность и простота.

      Это первая статья, в следующих статьях буду раскрывать архитектуру, прерывания, память, физическую, страничную, виртуальную, напишем алокатор. В самом ядре перегрузим глобально new и delete. И буду использовать STL контейнеры.

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

      Я не преподаватель или гуру писатель операционноек, энтузиаст с большим желанием, разбираться, вчитываться и делиться информацией.


  1. Jijiki
    01.12.2025 15:08

    как я понял там вся суть укладывается в самохостинге языка