Всем здрасте, и сегодня мы начнем наше прохождение через низкоуровневый кодинг - написание ОС. Сегодня мы напишем загрузчик (точнее конфиг к GRUB) и простенькое ядро, которое будет выводить "Hello OSDev!"
Что нам понадобится:
Linux (у меня Kali Linux 2025.1a)
i686-elf-gcc и i686-elf-ld (тык)
qemu-system-i386
nasm
grub-mkrescue
Шаг 1. Структура папок
Создадим несколько папок:
mkdir boot #тут будет лежать скрипт для линковки
mkdir bin #тут - готовые бинарники
mkdir kernel #само ядро
mkdir iso #здесь будем собирать ISO
mkdir iso/boot #файл ядра
mkdir iso/boot/grub #тут конфиги GRUB
Шаг 2. Загрузчик
Тут все просто - мы будем использовать GRUB. Но нам будет нужно написать к нему конфиг, чтобы он смог загружать именно наш загрузчик. Он будет лежать в iso/boot/grub
.Конфиг простой:
set timeout=0
set default=0
menuentry "Habr OS" {
multiboot /boot/kernel.bin
}
в 4 строчке "Habr OS"
- название нашей ОС, которая будет отображаться в меню выбора GRUB. Можете выбрать его сами (Но берите ТОЛЬКО английские буквы)
А теперь и сам наш загрузчик
; boot/loader.s
[org 0x7C00]
[bits 16]
KERNEL_OFFSET equ 0x1000 ; Адрес ядра в памяти (4 КБ после загрузчика)
start:
cli
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
sti
call load_kernel
call enable_pm
jmp 0x08:KERNEL_OFFSET ; 0x08 = селектор кода из GDT
; Загружаем ядро с диска (секторы 2-16)
load_kernel:
mov bx, KERNEL_OFFSET
mov ah, 0x02 ; BIOS: read sectors
mov al, 15 ; Читаем 15 секторов (7.5 КБ)
mov ch, 0 ; Cylinder 0
mov cl, 2 ; Sector 2 (после загрузчика)
mov dh, 0 ; Head 0
int 0x13
jc .error
ret
.error:
jmp $
; Переход в защищённый режим
enable_pm:
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 1
mov cr0, eax
ret
; GDT (минимальная, без ошибок)
gdt:
dq 0x0 ; Null descriptor
code_segment:
dw 0xFFFF ; Limit (low 16 bits)
dw 0x0000 ; Base (low 16 bits)
db 0x00 ; Base (middle 8 bits)
db 10011010b ; Flags: code, 32-bit
db 11001111b ; Flags: limit (high 4 bits), granularity
db 0x00 ; Base (high 8 bits)
data_segment:
dw 0xFFFF
dw 0x0000
db 0x00
db 10010010b ; Flags: data
db 11001111b
db 0x00
gdt_descriptor:
dw gdt_descriptor - gdt - 1 ; Size of GDT
dd gdt ; Address of GDT
times 510 - ($ - $$) db 0
dw 0xAA55
В нем мы в основном указываем GDT
Шаг 3. Ядро
Мы подошли к самому интересному - ядру. Создадим в папке kernel
файл kernel.c
. И тут мы столкнемся с тем, что использовать stdlib
... нельзя! Потому что мы используем кросс-компилятор и библиотек он таких не знает. И тем более для каждой ОС она своя.
Давайте опишем сначала все для Multiboot (то есть для GRUB)
#define MULTIBOOT_MAGIC 0x1BADB002
#define MULTIBOOT_FLAGS 0
typedef unsigned int u32
typedef struct {
u32 magic;
u32 flags;
u32 checksum;
} __attribute__((packed)) multiboot_header_t;
multiboot_header_t multiboot_header = {
MULTIBOOT_MAGIC,
MULTIBOOT_FLAGS,
-(MULTIBOOT_MAGIC + MULTIBOOT_FLAGS)
};
Для GRUB - макросы с флагами и адресом загрузки после GRUB и структура с этими данными
Дальше - стек
unsigned char stack[4096] __attribute__((aligned(16)));
unsigned char* stack_top = stack + sizeof(stack);
Тут мы описываем массив символов, который является стеком, и указатель на символ, идущий после стека
Далее у нас идет основная функция ядра - _start()
void _start() {
__asm__ volatile ("mov %0, %%esp" : : "r" (stack_top));
char* video_memory = (char*)0xB8000;
const char* message = "Hello OSDev! And hello Habr.ru!";
for (int i = 0; message[i] != '\0'; i++) {
video_memory[i * 2] = message[i];
video_memory[i * 2 + 1] = 0x07;
}
while(1);
}
Мы пишем через ассемблеровую вставку, что куча свободной памяти начинается с того места, где закончился стек. Далее - создаем переменную с адресом VGA-видеопамяти, создаем переменную с сообщением... Стоп. Зачем так много пробелов?? Это для переноса строки, ведь \n, \t и \v и т. п. НЕ работают в консоли (пока что). В цикле for
мы выводим символ, причем всегда четный элемент видеопамяти (вкл. 0) - сам символ, а нечетным - его цвет (0x07 - светло-серый на черном). А цикл while
позволяет не уйти процессору в дали дальние и выйти из памяти ядра.
Шаг 4. Сборка, запуск
Ах да, мы забыли про linker.ld
и сборочный скрипт! Вот linker.ld
(он в boot
лежит):
ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
SECTIONS {
. = 0x1000;
.text : {
*(.text._start)
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data)
}
.bss : {
*(COMMON)
*(.bss)
}
}
Тут отмечается точка старта программы.
Ну а теперь сборка! Для этого я использую спец. скрипт build.sh
. Вот его внутренности:
nasm -f bin boot/loader.s -o bin/loader.bin &&
i686-elf-gcc -c kernel/kernel.c -o bin/kernel.o -ffreestanding -m32 &&
i686-elf-ld -T boot/linker.ld -o bin/kernel.bin bin/kernel.o &&
cat bin/loader.bin bin/kernel.bin > bin/os.bin &&
cp bin/kernel.bin iso/boot/kernel.bin &&
grub-mkrescue -o bin/os.iso iso &&
echo "build succsess"
Первым делом мы собираем наш пользовательский загрузчик. Далее мы собираем ядро для bare metal и без подключения sdtlib
. Дальше линкуем пользовательский загрузчик с ядром, копируем в папку где генрируется ISO и генерируем сам ISO. Дальше можно уже засунуть его в QEMU (команда qemu-system-i386 -cdrom bin/os.iso -machine pc -d int -D qemu.log
) и посмотреть на те самые строчки, которые мы ждали...

Ну а далее на нашу ОС есть планы:
1. Реализуем ввод
2. Сделаем простые команды (типа shutdown
, echo
)
Спасибо что прочитали статью! Буду писать продолжение
Комментарии (39)
SIISII
18.08.2025 07:49Кстати говоря, если делать так, как сделано, -- считывать подряд N первых секторов, -- это, конечно, облегчает жизнь, но правильней было бы найти нужный файл в каталоге диска и прочитать его...
unsigned
char
stack[4096]
attribute
((aligned(16)));
Не знаю, как на чистых современных сях, а на це++ со стандарта 11 года выравнивание можно (и нужно) описывать стандартными средствами, а не с помощью нестандартных атрибутов -- гарантирует переносимость между различными компиляторами и всё такое.
ADD. Посмотрел: в сях добавили тоже, но лишь в стандарте 23 года (https://en.cppreference.com/w/c/language/alignas.html):
struct sse_t { alignas(16) float sse_data[4]; };
Mutar
18.08.2025 07:49И тут мы столкнемся с тем, что использовать
stdlib
... нельзя! Потому что мы используем кросс-компилятор и библиотек он таких не знает.Какой еще кросс-компилятор?
kaspary Автор
18.08.2025 07:49кросс-компилятор - это такой компилятор, который компилирует не под ОС, на которрой он стоит, а под bare metal (без ОС) определенной архитектуры, например i686, x86_64
Mutar
18.08.2025 07:49Я про то почему из-за компилятора у нас нету доступа к стандартному хедеру libc? Это не так работает! Тем более он не обязательно делает под bare metal.
DrArgentum
18.08.2025 07:49мне приходилось с автором общаться в одном чате. он активно использует LLM. вот поэтому, ему уже указывали на этот факт, но он не исправил
kaspary Автор
18.08.2025 07:49вы кто вообще
DrArgentum
18.08.2025 07:49Я могу дать конкретные ссылки
kaspary Автор
18.08.2025 07:49ссылки не надо, не имеет смысла
kaspary Автор
18.08.2025 07:49я честно говоря не знаю про libc.h, и да, необязательно под bare metal, но под архитектуру
zeroqxq
18.08.2025 07:49Честно говоря после прочтения статьи у меня остались очень смешанные чувство. Данный материал наноминает больше не какую либо статью , а скорее замтеки автора. Материал нормально не раскрыт, болтаются пустые куски кода, плоскость, однотипность статьи и негрмотность автора!
daytona13
18.08.2025 07:49Статья вызвала только смех и ничего более.
kaspary Автор
18.08.2025 07:49ну я так и говорил, что рейд будет
DrArgentum
18.08.2025 07:49Это не рейд, а нормальная реакция сообщества. Не надо выдавать себя за эксперта. Ваш код либо украден либо сгенерирован AI.
VyacheslavHere
18.08.2025 07:49Очередная статья псевдо-профи с ИИ знаниями. Есть статьи гораздо лучше. Lmao.
zeroqxq
18.08.2025 07:49Я уже писал комментарий к этой статье. Но после прочтения защиты автора от "рейда" мне показалось что кроме скопированного из AI кода автор еще и имеет не имеет чувства ситуации. И продолжает общаться как в чате
vi_is_raven
18.08.2025 07:49Статья объективно не соответствует качеству Хабра и представляет собой мешанину из шизы и нейробреда. Жирнейший минус.
RodionGork
Занятно :) напомнило как дизассемблировал код вируса на дискетах - в общем-то "хелло Хабр" можно и в этом стартовом секторе просто уместить, безо всякого Grub и "простенького ядра".
Я не к тому что "писать свою ОС" вредно - но чтобы она была как можно более "своей" там должно быть как можно менее готовых компонент :) Не серчайте за маленькую критику.
SIISII
А ещё неплохо, чтобы она была таки ОС, а не очередным ХеллоВорлдом прямо из загрузчика :)
denis_iii
Да, для этого нужен шедулер потоков с запуском, остановкой и переключением контекста.
Либо из POSIX API либо из ReactOS. Что бы можно было что-то компилировать и минимально запускать.
ps: ну и сокеты конечно, что бы минимальный interop был.
RodionGork
необязательно :) однозадачные ОС тоже в истории были...
kaspary Автор
я вторую часть просто не успел дописать, я там на другой площадке был, а тут и не уследил что приняли