Всем привет! Давно не писал статей и вот наконец решил вернуться.
Эта статья будет не столько о написании собственной операционной системы, сколько о личных переживаниях, которые, возможно, знакомы и вам. Может быть, она повлияет на ваше отношение к обучению и поможет не совершить тех же ошибок, которые совершил я.
Введение
Я Python-программист: пишу сайты, Telegram-ботов, парсеры и десктопные приложения. Раньше я никогда всерьёз не задумывался о том, чтобы изучить Си. Однако в 2025 году, когда после колледжа я поступил в университет, этот язык начали преподавать в рамках учебной программы.
Си всегда казался мне сложным и совершенно ненужным. Но когда я познакомился с ним ближе, оказалось, что язык не настолько сложен, как я представлял. При этом у меня всё ещё оставалось непонимание: где именно я могу его применять?
Что бы я ни придумал, почти всё можно было реализовать на Python. Поэтому я решил воплотить в жизнь давнюю мечту — написать собственное ядро операционной системы.
Сейчас мы живём во время, когда нейросети окружают нас буквально повсюду. Если ты ими не пользуешься, то в глазах многих выглядишь человеком, отставшим от технологий.
После появления нейросетей и обесценивания самостоятельного поиска информации я начал замечать, что боюсь терять время. Раньше я мог разбираться в незнакомом языке или читать код другого программиста, написанный «правильными» руками, чтобы понять, как работает какая-нибудь формула. В то время это был расчёт верхней и нижней недели — вы наверняка знаете, о чём речь, если учились в колледже или вузе.
Сейчас же мне страшно потратить лишний час. Возможно, дело в том, что почти в 21 год я, как мне кажется, так ничего глобального и не достиг. А может быть, мы просто живём в такое время. Но факт остаётся фактом: я начал деградировать.
Все знают, что умение искать информацию для программиста порой важнее знания конкретного языка. Однако многие перестают развивать этот навык, что приводит к проблемам, о которых я расскажу ниже.
Главная проблема сверхдоступной информации — её цена.
Когда нейросеть отвечает на запрос, она, как ни странно, отвечает именно на него. При этом она часто отбрасывает информацию, которую считает лишней. В результате ты получаешь минимальный объём знаний, необходимый для выполнения конкретной задачи.
Кроме того, снижается ценность полученной информации, из-за чего она крайне быстро исчезает из памяти.
Когда ты самостоятельно ищешь что-то по интересующей теме, тебе приходится читать книги, документацию и обсуждения на форумах. Всё это требует времени и усилий, поэтому для мозга такая информация оказывается ценнее прямого и уже пережёванного ответа нейросети.
Начало разработки
Наконец перейдём к разработке ядра.
Разработка ядра — очень сложный процесс, который требует большого количества знаний и времени. Своё ядро я разрабатывал как на Windows 11, так и на Arch Linux — в моменты, когда у меня не было доступа к основному компьютеру и под рукой оставался только старенький ноутбук с Linux.
Поэтому всё, о чём я буду писать дальше, можно применять как на компьютере под управлением Windows, так и на Linux.
Также прошу не воспринимать эту статью как полноценное руководство по написанию ядра или изучению языка Си. Я и сам всё ещё учусь и во многом могу говорить или поступать неправильно. Тем не менее у меня всё это работает.
Что нам понадобится для написания ядра?
Среда разработки.
Загрузчик.
Линковщик.
Среда разработки
Если вы планируете работать на Windows, сначала вам понадобится MSYS2.
После установки необходимо добавить утилиты, которые мы будем использовать в дальнейшей разработке. Для этого запустите среду MSYS2 UCRT64 и выполните следующую команду:
pacman -Syu pacman -S --needed base-devel git xorriso mtools nasm mingw-w64-ucrt-x86_64-toolchain mingw-w64-ucrt-x86_64-qemu
Проверка:
gcc --version make --version git --version qemu-system-x86_64 --version xorriso --version nasm --version
gcc/make → собрать ядро
xorriso → собрать ISO
QEMU → запустить виртуальный компьютер
Если вы работаете на Linux, то просто установите все эти утилиты через терминал.
Загрузчик
Чтобы наша операционная система могла работать, её необходимо правильно запустить. Для этого нужен загрузчик, который загрузит ядро в оперативную память, инициализирует его и позволит процессору начать с ним работать.
Конечно, можно написать собственный загрузчик, но на старте я советую не тратить на это время, поскольку есть вероятность быстро перегореть и забросить проект.
Я выбрал загрузчик Limine. Скажу честно: его мне посоветовал ИИ, а теперь уже я советую его вам.
Именно в этом месте мои рассуждения из введения связываются с разработкой собственного ядра. Как я уже говорил, Си я не знаю, а время стараюсь экономить, поэтому решил воспользоваться помощью ИИ.
Нейросеть не писала код за меня. Она лишь рассказывала, с чего начать разработку, объясняла синтаксис Си и помогала выстраивать архитектуру операционной системы.
Чтобы начать работать с Limine, сначала необходимо скопировать его репозиторий в каталог external/ внутри проекта.
cd external git clone https://github.com/limine-bootloader/limine.git cd limine
Теперь нам необходимо собрать загрузчик. Для этого используется команда make.
Из всех полученных файлов нам понадобится только limine.h. Его необходимо перенести в каталог kernel/ или в любую другую директорию, в которой вы собираетесь хранить исходный код ядра.
Далее нам потребуется конфигурационный файл, чтобы загрузчик понимал, как именно загружать наше ядро.
Для этого создайте каталог boot в корне проекта, а внутри него — файл limine.conf. В этот файл необходимо добавить следующий код:
timeout: 5 default_entry: 1 /SafeAssOS protocol: limine path: boot():/boot/kernel.elf
/SafeAssOS — это название нашей операционной системы.
protocol — протокол, по которому будет загружаться ядро. Мы выбираем limine, чтобы наше ядро не загружалось как Linux, Multiboot или EFI-приложение.
path — путь к файлу ядра внутри загрузочного ISO-образа. В нашем случае ядро будет находиться по адресу /boot/kernel.elf.
Линковщик
Следующий файл не относится непосредственно к коду ядра. Это инструкция для линковщика.
В руководстве OSDev Limine Bare Bones файл linker.ld выделяется отдельно. Он описывает, где секции ядра будут располагаться в памяти. Там также указано, что при использовании протокола Limine отдельный загрузочный stub на ассемблере не требуется, если используемый язык поддерживает соглашение о вызовах SysV x86.
Сначала создадим файл linker.ld в каталоге kernel/, а затем добавим в него следующий код:
OUTPUT_FORMAT(elf64-x86-64) OUTPUT_ARCH(i386:x86-64) ENTRY(kmain) PHDRS { limine_requests PT_LOAD; text PT_LOAD; rodata PT_LOAD; data PT_LOAD; } SECTIONS { . = 0xffffffff80000000; .limine_requests : { KEEP(*(.limine_requests_start)) KEEP(*(.limine_requests)) KEEP(*(.limine_requests_end)) } :limine_requests . = ALIGN(CONSTANT(MAXPAGESIZE)); .text : { *(.text .text.*) } :text . = ALIGN(CONSTANT(MAXPAGESIZE)); .rodata : { *(.rodata .rodata.*) } :rodata .note.gnu.build-id : { *(.note.gnu.build-id) } :rodata . = ALIGN(CONSTANT(MAXPAGESIZE)); .data : { *(.data .data.*) } :data .bss : { *(.bss .bss.*) *(COMMON) } :data /DISCARD/ : { *(.eh_frame*) *(.note .note.*) } }
Эта структура близка к linker script из руководства Limine Bare Bones. В нём ядро размещается в верхней части виртуального адресного пространства, отдельно выделяются секции .limine_requests, .text, .rodata, .data и .bss, а ненужные секции .eh_frame и .note отбрасываются.
Этот файл был полностью сгенерирован с помощью нейросети, поскольку он не относится непосредственно к коду ядра. Он нужен для того, чтобы загрузчик понимал, какие секции памяти необходимо выделить и по каким адресам они должны располагаться.
Инструкции для make
Теперь в корне проекта необходимо создать файл Makefile.
Makefile не является частью операционной системы. Это набор инструкций для утилиты make, который определяет, какие команды необходимо выполнить для сборки проекта.
После создания файла добавьте в него следующий код:
.RECIPEPREFIX := > PROJECT_NAME := SafeAssOS CC := clang LD := ld.lld QEMU := qemu-system-x86_64 BUILD_DIR := build ISO_ROOT := iso_root KERNEL_DIR := kernel KERNEL_ELF := $(BUILD_DIR)/kernel.elf ISO := $(BUILD_DIR)/$(PROJECT_NAME).iso ifeq ($(OS),Windows_NT) HOST_OS := windows LIMINE_ROOT := external/limine LIMINE_BIN := $(shell find $(LIMINE_ROOT) -maxdepth 3 -type f \( -name "limine.exe" -o -name "limine" \) | head -n 1) QEMU_DISPLAY := else HOST_OS := linux LIMINE_ROOT := /usr/share/limine LIMINE_BIN := $(shell command -v limine) QEMU_DISPLAY := -display gtk endif LIMINE_BIOS_CD := $(shell find $(LIMINE_ROOT) -type f -name "limine-bios-cd.bin" | head -n 1) LIMINE_BIOS_SYS := $(shell find $(LIMINE_ROOT) -type f -name "limine-bios.sys" | head -n 1) LIMINE_UEFI_CD := $(shell find $(LIMINE_ROOT) -type f -name "limine-uefi-cd.bin" | head -n 1) LIMINE_EFI := $(shell find $(LIMINE_ROOT) -type f -name "BOOTX64.EFI" | head -n 1) KERNEL_C := $(shell find $(KERNEL_DIR) -type f -name "*.c") KERNEL_OBJ := $(patsubst $(KERNEL_DIR)/%.c,$(BUILD_DIR)/%.o,$(KERNEL_C)) CFLAGS := \ -target x86_64-unknown-none \ -std=c11 \ -Wall \ -Wextra \ -ffreestanding \ -fno-builtin \ -fno-stack-protector \ -fno-stack-check \ -fno-lto \ -fno-pic \ -m64 \ -march=x86-64 \ -mabi=sysv \ -mno-red-zone \ -I$(KERNEL_DIR) \ -I$(KERNEL_DIR)/lib LDFLAGS := \ -m elf_x86_64 \ -nostdlib \ -static \ -z max-page-size=0x1000 \ -T $(KERNEL_DIR)/linker.ld .PHONY: all kernel iso run clean check-limine info all: iso info: > @echo "HOST_OS = $(HOST_OS)" > @echo "LIMINE_ROOT = $(LIMINE_ROOT)" > @echo "LIMINE_BIN = $(LIMINE_BIN)" > @echo "KERNEL_C = $(KERNEL_C)" > @echo "KERNEL_OBJ = $(KERNEL_OBJ)" check-limine: > @test -n "$(LIMINE_BIN)" || (echo "limine executable not found"; exit 1) > @test -n "$(LIMINE_BIOS_CD)" || (echo "limine-bios-cd.bin not found in $(LIMINE_ROOT)"; exit 1) > @test -n "$(LIMINE_BIOS_SYS)" || (echo "limine-bios.sys not found in $(LIMINE_ROOT)"; exit 1) > @test -n "$(LIMINE_UEFI_CD)" || (echo "limine-uefi-cd.bin not found in $(LIMINE_ROOT)"; exit 1) > @test -n "$(LIMINE_EFI)" || (echo "BOOTX64.EFI not found in $(LIMINE_ROOT)"; exit 1) $(BUILD_DIR): > mkdir -p $(BUILD_DIR) $(BUILD_DIR)/%.o: $(KERNEL_DIR)/%.c | $(BUILD_DIR) > mkdir -p $(dir $@) > $(CC) $(CFLAGS) -c $< -o $@ kernel: $(KERNEL_OBJ) > $(LD) $(LDFLAGS) $(KERNEL_OBJ) -o $(KERNEL_ELF) iso: check-limine kernel > rm -rf $(ISO_ROOT) > mkdir -p $(ISO_ROOT)/boot > mkdir -p $(ISO_ROOT)/EFI/BOOT > cp $(KERNEL_ELF) $(ISO_ROOT)/boot/kernel.elf > cp boot/limine.conf $(ISO_ROOT)/boot/limine.conf > cp $(LIMINE_BIOS_CD) $(ISO_ROOT)/boot/limine-bios-cd.bin > cp $(LIMINE_BIOS_SYS) $(ISO_ROOT)/boot/limine-bios.sys > cp $(LIMINE_UEFI_CD) $(ISO_ROOT)/boot/limine-uefi-cd.bin > cp $(LIMINE_EFI) $(ISO_ROOT)/EFI/BOOT/BOOTX64.EFI > xorriso -as mkisofs \ -b boot/limine-bios-cd.bin \ -no-emul-boot \ -boot-load-size 4 \ -boot-info-table \ --efi-boot boot/limine-uefi-cd.bin \ -efi-boot-part \ --efi-boot-image \ --protective-msdos-label \ $(ISO_ROOT) \ -o $(ISO) > $(LIMINE_BIN) bios-install $(ISO) run: iso > $(QEMU) -cdrom $(ISO) -m 256M $(QEMU_DISPLAY) clean: > rm -rf $(BUILD_DIR) $(ISO_ROOT)
Разберём основные части файла.
PROJECT_NAME := SafeAssOS — название проекта.
Пути и используемые инструменты:
CC := clang— компилятор языка Си.LD := ld.lld— линковщик, используемый для сборки ядра с Limine.BUILD_DIR := build— каталог, в котором будут храниться результаты сборки.ISO_ROOT := iso_root— корневой каталог будущего ISO-образа.KERNEL_DIR := kernel— каталог с исходным кодом ядра.LIMINE_DIR := external/limine— путь к загрузчику Limine.
Сильно концентрироваться на этом файле мы не будем, поскольку он также не относится непосредственно к коду ядра. Makefile нужен только для того, чтобы система сборки понимала, как собирать наш проект.
Разработка ядра
Самая неинтересная часть статьи закончилась, и теперь мы наконец переходим к разработке ядра.
На данный момент написанное мной ядро обладает следующим функционалом:
вывод закрашенного прямоугольника;
вывод текста;
ввод с клавиатуры;
вывод данных;
создание файлов;
запись данных в файлы;
вывод содержимого файлов.
Начнём с главного файла — main.c.
Все файлы с исходным кодом будут располагаться в каталоге kernel/.
В начале файла обязательно необходимо добавить директиву:
#include "limine.h"
Limine будет обрабатывать наши запросы и заполнять структуры с ответами. Благодаря этому мы сможем получать необходимые данные, например информацию из видеопамяти, и работать с ними.
Далее в файл необходимо добавить следующий код:
__attribute__((used, section(".limine_requests"))) static volatile uint64_t limine_base_revision[] = LIMINE_BASE_REVISION(6); __attribute__((used, section(".limine_requests_start"))) static volatile uint64_t limine_requests_start_marker[] = LIMINE_REQUESTS_START_MARKER; __attribute__((used, section(".limine_requests"))) static volatile struct limine_framebuffer_request framebuffer_request = { .id = LIMINE_FRAMEBUFFER_REQUEST_ID, .revision = 0 }; __attribute__((used, section(".limine_requests_end"))) static volatile uint64_t limine_requests_end_marker[] = LIMINE_REQUESTS_END_MARKER; static void halt(void) { for (;;) { __asm__ volatile ("hlt"); } }
Этот код нужен для того, чтобы необходимые нам переменные и структуры не удалялись из памяти и помещались в соответствующие секции.
Функция halt() используется для «остановки» системы. Поскольку мы пишем ядро, после завершения главной функции программе некуда передавать управление. Поэтому нам необходимо остановить процессор до наступления следующего события.
Теперь создадим главную функцию ядра — kmain():
void kmain(void) { if (LIMINE_BASE_REVISION_SUPPORTED(limine_base_revision) == false) { halt(); } if (framebuffer_request.response == NULL) { halt(); } if (framebuffer_request.response->framebuffer_count < 1) { halt(); } halt(); }
В этой функции мы выполняем несколько проверок:
проверяем, передал ли Limine данные о видеопамяти:
if (framebuffer_request.response == NULL)
проверяем наличие хотя бы одного подключённого монитора:
if (framebuffer_request.response->framebuffer_count < 1)
проверяем, поддерживается ли используемая версия протокола Limine:
if (LIMINE_BASE_REVISION_SUPPORTED(limine_base_revision) == false)
На этом этапе мы уже можем собрать наше ядро и запустить его:
make clean make kernel make iso make run
После запуска мы ничего не увидим, поскольку в нашем ядре пока не реализованы функции вывода на экран.
До этого момента мы выполняли самую простую часть работы: практически не затрагивали код самого ядра.
Теперь создадим модуль для работы с framebuffer и реализуем первый вывод на экран.
В каталоге kernel/ создадим два файла:
framebuffer.c;framebuffer.h.
В файл framebuffer.c добавим следующий код:
#include "framebuffer.h" #include "limine.h" #include <stdint.h> static struct limine_framebuffer *current_fb; void fb_init(struct limine_framebuffer *framebuffer) { current_fb = framebuffer; } void fb_put_pixel(uint64_t x, uint64_t y, uint32_t color) { if (current_fb == 0) { return; } if (x > current_fb->width || y > current_fb->height) { return; } uint8_t *pixel_addres = (uint8_t *)current_fb->address + y * current_fb->pitch + x * 4; volatile uint32_t *pixel = (volatile uint32_t *)pixel_addres; *pixel = color; }
Сначала создадим структуру с указателем на структуру нашего framebuffer. Указатель нужен потому, что мы будем напрямую изменять framebuffer, а не работать с его копией.
В первой функции мы просто инициализируем буфер, чтобы затем использовать его в остальных функциях.
Во второй функции реализуется закрашивание пикселя по координатам x и y. Для этого необходимо получить адрес нужного пикселя в памяти и записать его в указатель *pixel_address.
Сначала из структуры current_fb мы получаем адрес начала видеопамяти. Затем с помощью умножения y на current_fb->pitch переходим к нужной строке экрана. Значение pitch указывает количество байт, которое занимает одна строка.
После этого переходим к нужному пикселю внутри строки, умножая x на 4, поскольку один пиксель занимает четыре байта.
Чтобы закрасить пиксель, по полученному адресу необходимо записать значение цвета.
В результате мы получили функцию для закрашивания отдельного пикселя. Чтобы использовать её в main.c, сначала необходимо заполнить заголовочный файл framebuffer.h.
#ifndef FRAMEBUFFER_H #define FRAMEBUFFER_H #include <stdint.h> #include "limine.h" void fb_init(struct limine_framebuffer *framebuffer); void fb_put_pixel(uint64_t x, uint64_t y, uint32_t color); #endif
Теперь переходим к main.c и после нашего кода (перед вызовом halt();) пишем следующее:
struct limine_framebuffer *fb = framebuffer_request.response->framebuffers[0]; fb_init(fb); fb_put_pixel(4, 4, 0x80FF0000);
Теперь, если приглядеться, то можно увидеть что на черном фоне появилась красная точка.

Если можно выводить пиксель, то можно рисовать фигуры. Следующим этапом мы нарисуем прямоугольник. Создаем функцию fb_fill_rect и пишем такой код:
void fb_fill_rect(uint64_t x, uint64_t y, uint64_t width, uint64_t height, uint32_t color) { uint64_t end_x = x + width; uint64_t end_y = y + height; for (uint64_t py = y; py < end_y; py++) { for (uint64_t px = x; px < end_x; px++) { fb_put_pixel(px, py, color); } } }
Здесь у функции появляются параметры ширины и высоты прямоугольника. Чтобы его нарисовать, нам необходимо знать начальные и конечные координаты.
С помощью первого цикла мы перемещаемся по координате y до конечной точки, а с помощью второго, вложенного цикла — по координате x.
Внутри второго цикла вызывается функция закрашивания отдельного пикселя. В результате она последовательно закрашивает всю область между указанными координатами.
Как и функцию закрашивания пикселя, функцию рисования прямоугольника необходимо вызвать внутри kmain(). После сборки и запуска ядра мы получим следующий результат:

Заключение
Теперь вы умеете собирать собственное ядро и писать для него первые функции.
Если я увижу, что эта тема вам интересна, то выпущу вторую часть. В ней я расскажу, как реализовать вывод текста, добавить форматированный вывод, создать подобие терминала и использовать «встроенную» библиотеку, которую я начал писать специально для этого ядра.
Комментарии (13)

Coocos
17.07.2026 19:11Выглядит как дешевая и бесполезная информация. Лучше зайти на гитхаб и почитать исходники реальных операционных систем. Если хочется использовать ИИ - можно попросить ИИ объяснить существующий код. Набор каких-то непонятных фактов и рекомендаций от человека, делающего что-то впервые. Какая от этого польза? Очередной мусор. Фу.

SyncSay
17.07.2026 19:11Да боже, вас кто-то заставляет читать? Человек поделился опытом, в чём проблема?

Coocos
17.07.2026 19:11Проблема в том, что с ростом количества таких статей падает общее качество материалов. Труднее становится найти качественные и интересные материалы. затем приходят следующие авторы и снова гонят нейрослоп.

codecity
17.07.2026 19:11Теперь так может каждый - по этому ценность такой информации равна нулю. Когда раньше писал сам и разбирался с нуля сам - лучше в голове держалось. Сейчас вроде и понял, если что не понятно - спросил и вроде понял. Но через пару-тройку месяцев уже как будто это было не с тобой...

Andrey2008
17.07.2026 19:11Я так понимаю, речь идёт об этом проекте OS. Ну что же, автору предстоит узнать ещё много о языке С. Например, что такое выход за границу буфера.
void terminal_write_hex(uint64_t hex_num) { uint64_t index_buffer = 0; char buffer[sizeof(uint64_t)]; if (hex_num == 0x0) { terminal_write("0x0"); return; } terminal_write("0x"); while (hex_num > 0) { uint64_t digit = hex_num % 16; if (digit >= 10) { digit = digit - 10; buffer[index_buffer] = 'A'+digit; }else { buffer[index_buffer] = '0' + digit; } hex_num = hex_num / 16; index_buffer++; } .....Какой милый баг. Обратите внимание на размер буфера. Мда, интересные времена "безопасного ПО" настают. Простите за едкость комментария, но не удержался, увидев проект автора.
Наш проект — это платформа для обеспечения и сохранения безопасности информации в цифровой среде, а также для развития независимой IT-экосистемы.
Мы создаём комплекс программного обеспечения, ориентированный на защиту данных, открытость технологий и поддержку русскоязычного IT-сообщества.
P.S. Приходите узнать про нормальную разработку безопасного, надёжного ПО.

Ka463
17.07.2026 19:11Ну хз, давным давно лет 20 назад, когда я этим интересовался, я на асме писал свое ядро начиная с инициализации прерываний, управлением флоповодом, чтения секторов с дискеты, перевод процессора в 32 битный режим, инициализация ascii и т.д вот это интересно, а не взять готовое и что то там слинковать, не понимая как работает железо

Andrey2008
17.07.2026 19:11uint64_t kstrncmp(char *str1, char *str2, uint64_t len){ uint64_t index_str = 0; uint64_t different_char = 0; while (index_str < len) { if (str1[index_str] != str2[index_str]) { different_char++; } index_str++; } return different_char; }

Cheef44 Автор
17.07.2026 19:11Вы все такие умные и профессиональные, но не поняли суть статьи. Это демонстрация ситуации, когда человек полагается на ии, вместо глубокого изучения технологий. Суть в том, что я осознал проблему, начал изучать архитектуру компьютеров и язык Си. Это не гайд, так делать не надо, это мой личный путь от программиста который свернул не туда и возвращается на правильный путь.
Вы вместо того чтобы разделить со мной эти переживания, глумитесь выставляя себя более хорошими программистами. Я знаю что то что я написал даже ядром назвать сложно, что тут куча ошибок, что я плохо знаю язык C, но мне хватило сил это заметить и начать разбираться в этой теме. Эта статья предназначена для людей которые совершили такую же ошибку как и я.
Очень грустно наблюдать за тем, что люди не выкупили того что я не продвигаю такое написание кода, грустно что вместо поддержки вы пытаетесь задеть. Я не знаю какую цель вы преследуете, может хотите меньше конкуренции или поднять свою самооценку за счет более слабых программистов. Знайте что мне не стыдно за мой путь. Лучше совершить кучу ошибок и понять как все работает, чем сидеть и ничего не делать
YuriPanchul
А вы хотя-бы понимаете каждый файл который выдал вам AI? Оно делает ошибки и в линкер скриптах и в стартапе, и в других вещах. Если вы этого не понимаете, вы пропустите его ошибки.
Cheef44 Автор
Примерно понимаю. Я сейчас начал глобальное изучение этой темы уже по книгам и документациям. Когда разберусь в этом, обязательно проверю все файлы сделанные AI. Явных ошибок пока не встречал