Почему ядро зависает?
Решил погрузиться в разработку на низком уровне. Изучил основы NASM, решил сразу перейти к разработке ОС (да, я идиот).
Изначально написал основу ядра ОС на C++, а потом решил писать загрузчик на asm (явно не лучшее решение).
Что имею по итогу? Загрузчик, который не работает и базу на cpp.
Загрузчик писал изначально сам, но успеха не было. С помощью Copilot'а и ChatGPT продвинулся дальше. Понял свои изначальные ошибки. Сейчас загрузчик на +-60% написан ИИшками. Но основная проблема так и не решена.
Ядро зависает после вывода 3 сообщений:

После этого, как я задумывал, должно выводиться лого ОС и переход к основному циклу (сейчас вывод лого вырезан). Но этого не происходит.
Прошу совета от экспертов и максимальной критики. На что стоит обратить внимание, что сделано неправильно, насколько всё у меня плохо?
Основные скрипты:
#include "../include/loading_screen.h"
#include "../include/keyboard.h"
#include "../include/video.h"
extern char bss_start[], bss_end[];
void zero_bss() {
for (char *p = bss_start; p < bss_end; ++p) *p = 0;
}
void main_loop(){
print_text(">");
while (1) {
asm volatile ("hlt");
}
}
extern "C" void kernel_main() __attribute__((section(".text.entry")));
extern "C" void kernel_main() {
clear_screen();
asm volatile ("mov $'K', %%al; out %%al, $0xE9" : : : "al");
zero_bss();
main_loop();
}
<br>
[org 0x7C00]<br>
[bits 16]<br>
<br>
start:<br>
; ----------------------------------------<br>
; 1. Инициализация сегментов и стека<br>
; ----------------------------------------<br>
mov al, 'R'<br>
out 0xE9, al<br>
<br>
xor ax, ax<br>
mov ds, ax<br>
mov es, ax<br>
mov ss, ax<br>
mov sp, 0x7C00 ; стек под загрузчиком<br>
<br>
; Сохраняем номер диска, с которого загрузились (BIOS передаёт в DL)<br>
mov [boot_drive], dl<br>
<br>
; Печатаем сообщение<br>
mov si, msg_loading<br>
call print_string<br>
<br>
; ----------------------------------------<br>
; 2. Загрузка ядра с дискеты в 0x10000<br>
; (используем CHS: цилиндр 0, головка 0, сектора 2..)<br>
; ----------------------------------------<br>
mov ax, 0x1000 ; ES:BX = 0x1000:0x0000 → физический адрес 0x10000<br>
mov es, ax<br>
xor bx, bx<br>
<br>
mov ah, 0x02 ; функция чтения секторов<br>
mov al, 128 ; читаем 64 сектора = 32 КБ (достаточно для маленького ядра)<br>
mov ch, 0 ; цилиндр 0<br>
mov cl, 2 ; начальный сектор = 2 (первый сектор после загрузчика)<br>
mov dh, 0 ; головка 0<br>
mov dl, [boot_drive] ; диск, с которого загружены<br>
int 0x13<br>
jc read_error<br>
<br>
mov si, msg_ok<br>
call print_string<br>
<br>
; ----------------------------------------<br>
; 3. Включение A20<br>
; ----------------------------------------<br>
call enable_a20<br>
<br>
; ----------------------------------------<br>
; 4. Переход в защищённый 32‑битный режим<br>
; ----------------------------------------<br>
cli<br>
lgdt [gdt_descriptor]<br>
<br>
mov eax, cr0<br>
or eax, 1<br>
mov cr0, eax<br>
<br>
mov al, 'r'<br>
out 0xE9, al<br>
<br>
jmp CODE_SEG32:protected_mode<br>
<br>
; ===========================================================================<br>
; 32‑битный защищённый режим<br>
; ===========================================================================<br>
[bits 32]<br>
protected_mode:<br>
mov al, 'P'<br>
out 0xE9, al<br>
<br>
; Загружаем сегменты данных<br>
mov ax, DATA_SEG32<br>
mov ds, ax<br>
mov ss, ax<br>
mov es, ax<br>
mov fs, ax<br>
mov gs, ax<br>
<br>
; Временный стек (под 1 МБ)<br>
mov esp, 0x9C00<br>
<br>
; ----------------------------------------<br>
; 5. Копируем ядро из 0x10000 → 0x100000<br>
; (64 сектора = 32768 байт = 8192 двойных слова)<br>
; ----------------------------------------<br>
mov esi, 0x10000<br>
mov edi, 0x100000<br>
mov ecx, 8192 ; 32 КБ / 4<br>
cld<br>
rep movsd<br>
<br>
; ----------------------------------------<br>
; 6. Настройка страничных таблиц (identity mapping первых 2 МБ)<br>
; Используем память по адресам 0x7000, 0x7100, 0x7200<br>
; (они не заняты загрузчиком и ядром)<br>
; ----------------------------------------<br>
; Обнуляем PML4 (0x7000)<br>
mov edi, 0x7000<br>
mov ecx, 1024<br>
xor eax, eax<br>
rep stosd<br>
<br>
; PML4[0] → PDP (0x7100) с флагами присутствия и записи<br>
mov dword [0x7000], 0x7103<br>
<br>
; Обнуляем PDP (0x7100)<br>
mov edi, 0x7100<br>
mov ecx, 1024<br>
rep stosd<br>
<br>
; PDP[0] → PD (0x7200)<br>
mov dword [0x7100], 0x7203<br>
<br>
; Обнуляем PD (0x7200)<br>
mov edi, 0x7200<br>
mov ecx, 1024<br>
rep stosd<br>
<br>
; PD[0] = 2‑МБ страница, identity mapping (физический адрес 0 → 0)<br>
; Флаги: присутствует, доступна для записи, размер страницы 2 МБ (PS=1)<br>
mov dword [0x7200], 0x83<br>
<br>
; Загружаем CR3<br>
mov eax, 0x7000<br>
mov cr3, eax<br>
<br>
; ----------------------------------------<br>
; 7. Включение PAE и долгого режима<br>
; ----------------------------------------<br>
; PAE в CR4<br>
mov eax, cr4<br>
or eax, 1 << 5<br>
mov cr4, eax<br>
<br>
; EFER.LME = 1<br>
mov ecx, 0xC0000080<br>
rdmsr<br>
or eax, 1 << 8<br>
wrmsr<br>
<br>
; Включаем пейджинг (CR0.PG)<br>
mov eax, cr0<br>
or eax, 1 << 31<br>
mov cr0, eax<br>
<br>
mov al, 'p'<br>
out 0xE9, al<br>
<br>
; ----------------------------------------<br>
; 8. Прыжок в 64‑битный сегмент кода<br>
; ----------------------------------------<br>
jmp CODE_SEG64:long_mode<br>
<br>
; ===========================================================================<br>
; 64‑битный долгий режим<br>
; ===========================================================================<br>
[bits 64]<br>
long_mode:<br>
<br>
mov al, 'L'<br>
out 0xE9, al<br>
<br>
; Загружаем сегменты данных (в долгом режиме они практически не используются,<br>
; но должны быть корректными)<br>
mov ax, DATA_SEG64<br>
mov ds, ax<br>
mov ss, ax<br>
mov es, ax<br>
mov fs, ax<br>
mov gs, ax<br>
<br>
; Устанавливаем указатель стека<br>
; Было 0x9C00 - мало<br>
; Было 0x200000 - много<br>
mov rsp, 0x1FF000<br>
<br>
and rsp, -16<br>
<br>
mov al, 'l'<br>
out 0xE9, al<br>
<br>
mov rax, 0x100000<br>
jmp rax<br>
<br>
; Если ядро вернёт управление – зависаем<br>
cli<br>
hlt<br>
jmp $<br>
<br>
[bits 16]<br>
; ===========================================================================<br>
; Вспомогательные процедуры (реальный режим)<br>
; ===========================================================================<br>
<br>
; ---------- Печать строки с нулевым окончанием ----------<br>
print_string:<br>
lodsb<br>
or al, al<br>
jz .done<br>
mov ah, 0x0E<br>
int 0x10<br>
jmp print_string<br>
.done:<br>
ret<br>
<br>
; ---------- Ошибка чтения ----------<br>
read_error:<br>
mov si, msg_error<br>
call print_string<br>
cli<br>
hlt<br>
jmp $<br>
<br>
; ---------- Включение A20 ----------<br>
enable_a20:<br>
mov al, 'A'<br>
out 0xE9, al<br>
<br>
; Сначала пробуем через BIOS (int 15h, ax=2401h)<br>
mov ax, 0x2401<br>
int 0x15<br>
jnc .done<br>
<br>
mov al, 'a'<br>
out 0xE9, al<br>
<br>
; Если не вышло – через контроллер клавиатуры (стандартная процедура)<br>
call enable_a20_keyboard<br>
.done:<br>
ret<br>
<br>
enable_a20_keyboard:<br>
; Отключаем клавиатуру<br>
call a20_wait_input<br>
mov al, 0xAD<br>
out 0x64, al<br>
<br>
call a20_wait_input<br>
mov al, 0xD0<br>
out 0x64, al<br>
<br>
call a20_wait_output<br>
in al, 0x60<br>
push eax ; error<br>
<br>
call a20_wait_input<br>
mov al, 0xD1<br>
out 0x64, al<br>
<br>
call a20_wait_input<br>
pop eax<br>
or al, 2<br>
out 0x60, al<br>
<br>
call a20_wait_input<br>
mov al, 0xAE<br>
out 0x64, al<br>
<br>
call a20_wait_input<br>
ret<br>
<br>
a20_wait_input:<br>
in al, 0x64<br>
test al, 2<br>
jnz a20_wait_input<br>
ret<br>
<br>
a20_wait_output:<br>
in al, 0x64<br>
test al, 1<br>
jz a20_wait_output<br>
ret<br>
<br>
; ===========================================================================<br>
; Данные<br>
; ===========================================================================<br>
msg_loading db "Loading kernel...", 13, 10, 0<br>
msg_ok db "OK.", 13, 10, 0<br>
msg_error db "Read error!", 13, 10, 0<br>
<br>
boot_drive db 0 ; номер диска, с которого загружены<br>
<br>
; ===========================================================================<br>
; GDT (глобальная таблица дескрипторов)<br>
; Содержит дескрипторы для 32‑битного и 64‑битного режимов<br>
; ===========================================================================<br>
gdt_start:<br>
; нулевой дескриптор (обязателен)<br>
dq 0<br>
<br>
; 32‑битный сегмент кода (base=0, limit=0xFFFFF, DPL=0, 32-bit, исполняемый)<br>
dw 0xFFFF<br>
dw 0x0000<br>
db 0x00<br>
db 0x9A<br>
db 0xCF<br>
db 0x00<br>
<br>
; 32‑битный сегмент данных (base=0, limit=0xFFFFF, DPL=0, доступный для чтения/записи)<br>
dw 0xFFFF<br>
dw 0x0000<br>
db 0x00<br>
db 0x92<br>
db 0xCF<br>
db 0x00<br>
<br>
; 64‑битный сегмент кода (base=0, limit=0xFFFFF, L=1, исполняемый)<br>
dw 0xFFFF<br>
dw 0x0000<br>
db 0x00<br>
db 0x9A<br>
db 0xAF ; L=1, D=0, G=1<br>
db 0x00<br>
<br>
; 64‑битный сегмент данных (base=0, limit=0xFFFFF, L=0, доступный для чтения/записи)<br>
dw 0xFFFF<br>
dw 0x0000<br>
db 0x00<br>
db 0x92<br>
db 0xAF ; G=1, остальное как обычно<br>
db 0x00<br>
<br>
gdt_end:<br>
<br>
gdt_descriptor:<br>
dw gdt_end - gdt_start - 1 ; размер GDT - 1<br>
dd gdt_start ; линейный адрес GDT<br>
<br>
; Селекторы (смещения от начала GDT)<br>
CODE_SEG32 equ 0x08<br>
DATA_SEG32 equ 0x10<br>
CODE_SEG64 equ 0x18<br>
DATA_SEG64 equ 0x20<br>
<br>
; ===========================================================================<br>
; Заполнение до 510 байт и сигнатура загрузочного сектора<br>
; ===========================================================================<br>
times 510 - ($ - $$) db 0<br>
dw 0xAA55<br>
OUTPUT_FORMAT(elf64-x86-64)
ENTRY(kernel_main)
SECTIONS
{
. = 0x100000;
.text.entry : {
*(.text.entry)
}
.text : {
*(.text)
}
.rodata : {
*(.rodata) *(.rodata.*)
}
.data : {
*(.data)
}
.bss : {
bss_start = .;
*(.bss)
bss_end = .;
}
}
Предоставленные скрипты уже с различными экспериментальными фиксами, которые не работают.