Задать вопрос
@SushkaCraftHabr

Почему ядро зависает и не запускается до конца?

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

c5740c002553faf0592d7d370498f60e.png
После этого, как я задумывал, должно выводиться лого ОС и переход к основному циклу (сейчас вывод лого вырезан). Но этого не происходит.
Прошу совета от экспертов и максимальной критики. На что стоит обратить внимание, что сделано неправильно, насколько всё у меня плохо?
Основные скрипты:

#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 = .;
    }
}

Предоставленные скрипты уже с различными экспериментальными фиксами, которые не работают.
  • Вопрос задан
  • 216 просмотров
Подписаться 2 Сложный Комментировать
Помогут разобраться в теме Все курсы
  • Нетология
    Разработчик на C++
    12 месяцев
    Далее
  • Яндекс Практикум
    Разработчик C++
    9 месяцев
    Далее
  • Академия Эдюсон
    Разработчик игр на Unreal Engine + ИИ
    9 месяцев
    Далее
Решения вопроса 1
opium
@opium
Просто люблю качественно работать
такое чувство что до kernel_main дело даже не доходит. PML4 у тебя на 0x7000, и зануляешь ты сразу 4КБ (0x7000-0x7FFF) — а сам бутлоадер с GDT сидит на 0x7C00, то есть стираешь свой же код во время исполнения. Плюс 0x7100 и 0x7200 не выровнены на 4КБ — CPU в PML4/PDP-энтри берёт только биты [51:12], так что PDP и PD физически схлопнутся в тот же 0x7000. Возьми адреса подальше и выровненные, например 0x20000/0x21000/0x22000.

p.s. проверь ещё как кладёшь ядро на диск — если это сырой .elf без objcopy -O binary, прыжок на 0x100000 попадёт в заголовок ELF, а не в kernel_main
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы