Другое

Bootloader замерзает при ядре >512 байт: решение

Узнайте, почему ваш x86 bootloader замерзает, когда ядро превышает 512 байт, и как решить проблему, загружая более крупные ядра в собственном ОС для разработки.

Почему мой загрузчик зависает, когда размер ядра превышает 512 байт?

Я разрабатываю собственный x86‑загрузчик (cheeseDOS) и сталкиваюсь с проблемой: загрузчик зависает во время процесса загрузки, если размер ядра превышает 512 байт. Загрузчик успешно загружает и выполняет, когда ядро 404 байта или меньше, но зависает на шаге «Entering protected mode…» когда ядро 2,2 КБ.

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

assembly
; stage1.asm code
[ORG 0x7C00]
[BITS 16]

start:
    push cs
    pop ds
    mov ax, 0x0003
    int 0x10

    mov ah, 0x01
    mov cx, 0x2000
    int 0x10

    mov si, loading
    call print
    
    mov ax, 0x0000
    mov es, ax
    mov bx, 0x7E00
    mov ah, 0x02
    mov al, 1
    mov ch, 0
    mov cl, 4
    mov dh, 0
    mov dl, [BootDrive]
    int 0x13
    jc disk_error
    
    mov si, success
    call print
    jmp 0x0000:0x7E00

disk_error:
    mov si, error
    call print
    call print_hex
    call newline

    jmp $

print:
    pusha
.loop:
    lodsb
    test al, al
    jz .done
    mov ah, 0x0E
    mov bh, 0x00
    mov bl, 0x07
    int 0x10
    jmp .loop
.done:
    popa
    ret

print_hex:
    pusha
    mov bl, al
    shr al, 4
    call .nibble
    mov al, bl
    and al, 0x0F
    call .nibble
    popa
    ret
.nibble:
    cmp al, 9
    jbe .digit
    add al, 7
digit:
    add al, 0x30
    mov ah, 0x0E
    int 0x10
    ret

newline:
    pusha
    mov al, 0x0D
    mov ah, 0x0E
    int 0x10
    mov al, 0x0A
    mov ah, 0x0E
    int 0x10
    popa
    ret

loading db "Loading stage 2...", 0x0D, 0x0A, 0
success db "Starting stage 2...", 0x0D, 0x0A, 0
error db "Disk error: 0x", 0

BootDrive db 0

times 510-($-$$) db 0
dw 0xAA55
assembly
; stage2.asm code
ORG 0x7E00

[BITS 16]
stage2:   
   cli
   mov [BootDrive], dl
   xor ax, ax       
   mov ds, ax             
   mov ss, ax             
   mov sp, 0x9c00
   mov si, enable_a20
   call print
   call a20
   mov si, set_idt
   call print
   call setup_idt
   mov si, set_gdt
   call print
   lgdt [gdtinfo]         
   mov si, enter_pmode
   call print
   mov eax, cr0          
   or al, 1
   mov cr0, eax
   mov bx, 0x10
   mov ds, bx
   mov es, bx
   mov fs, bx
   mov gs, bx
   mov ss, bx
   and al, 0xFE            
   mov cr0, eax
   xor ax, ax
   mov ds, ax
   mov ss, ax
   mov sp, 0x9c00
   sti                     
   mov si, enter_unreal
   call print
   cli                     
   jmp boot

boot:
    mov si, load_disk
    call print
    mov dl, [BootDrive]
    xor ax, ax
    int 0x13
    jc disk_error
    mov ax, 0x1000
    mov es, ax
    mov bx, 0
    mov dl, [BootDrive]
    mov dh, 0
    mov ch, 0
    mov cl, 4
    mov al, 16
    mov ah, 0x02
    int 0x13
    jc disk_error
    cmp al, 0
    je disk_error
    mov ax, [es:0]
    cmp ax, 0
    je no_kernel
    mov si, enter_pmode
    call print
    lidt [idtinfo]
    lgdt [gdtinfo]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp 0x08:pmode32

setup_idt:
   push ax
   push cx
   push di
   push es
   xor ax, ax
   mov es, ax
   mov di, idt
   mov cx, 256
.loop:
   mov eax, default_isr
   mov [es:di], ax
   add di, 2
   mov word [es:di], 0x08
   add di, 2
   mov byte [es:di], 0
   inc di
   mov byte [es:di], 0x8E
   inc di
   shr eax, 16
   mov [es:di], ax
   add di, 2
   loop .loop
   pop es
   pop di
   pop cx
   pop ax
   ret

print:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    mov bh, 0x00
    mov bl, 0x07
    int 0x10
    jmp print
done:
    ret

a20:
    in al, 0x64
    test al, 2
    jnz a20
    mov al, 0xD1
    out 0x64, al
.wait1:
    in al, 0x64
    test al, 2
    jnz .wait1
    mov al, 0xDF
    out 0x60, al
.wait2:
    in al, 0x64
    test al, 2
    jnz .wait2
    ret

gdtinfo:
   dw gdt_end - gdt - 1   
   dd gdt                 

gdt:    
   dd 0,0                 

codedesc:                 
   dw 0xFFFF              
   dw 0x0000              
   db 0x00                
   db 10011010b           
   db 11001111b           
   db 0x00                

flatdesc:                 
   dw 0xFFFF              
   dw 0x0000              
   db 0x00                
   db 10010010b           
   db 11001111b           
   db 0x00                

gdt_end:

idtinfo:
   dw (256 * 8) - 1       
   dd idt                 

set_idt db "Setting IDT...", 0x0D, 0x0A, 0
enter_pmode db "Entering protected mode...", 0x0D, 0x0A, 0
set_gdt db "Setting GDT...", 0x0D, 0x0A, 0
enter_unreal db "Entering unreal mode...", 0x0D, 0x0A, 0
load_disk db "Loading kernel...",0x0D, 0x0A, 0
no_kernel db "No kernel found!", 0x0D, 0x0A, 0
disk_error db "Disk read error!", 0x0D, 0x0A, 0
enable_a20 db "Enabling A20...", 0x0D, 0x0A, 0
BootDrive db 0

[BITS 32]
pmode32:
    mov ax, 0x10           
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x9c00    
    mov edi, 0xB8000 + (8 * 160)
    mov esi, start_kernel
    mov ah, 0x07
.loop:
    lodsb
    test al, al
    jz .done
    stosw
    jmp .loop
done:
    jmp 0x08:0x10000

default_isr:
    iretd

start_kernel db "Starting kernel...",0

align 8
idt:
   times (256 * 8) db 0

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

Ваш загрузчик зависает, когда ядро превышает 512 байт, из‑за фундаментального ограничения архитектуры x86: BIOS загружает только первые 512 байт загрузочного сектора. Как только ваше ядро становится больше этого размера, система не может корректно загрузить его с использованием текущего подхода, что приводит к зависанию на этапе «Entering protected mode…».

Проблема возникает из‑за того, как работает загрузка x86 – BIOS загружает только первый сектор (512 байт) в память по адресу 0x7C00, а затем выполняет его. Ваш stage1 загрузчик правильно загружает stage2, но механизм загрузки ядра в stage2 не обрабатывает файлы большего размера корректно.

Содержание

Понимание ограничения в 512 байт

Ограничение в 512 байт – фундаментальное ограничение архитектуры x86. Согласно OSDev Wiki, «на x86 обычно ограничены 512 байт для первого этапа». Это не произвольное ограничение – оно определено форматом загрузочного сектора:

  • Структура загрузочного сектора: первые 512 байт содержат код загрузки
  • Поведение BIOS: BIOS читает ровно один сектор (512 байт) с диска по смещению 0
  • Главный загрузочный сектор (MBR): загрузочный сектор должен завершаться сигнатурой 0xAA55

Как объясняет Алекс Паркер, «BIOS загружает только первые 512 байт загрузочного сектора. Если мы хотим писать программы больше 512 байт… нам придется загружать больше с диска».

Анализ кода вашего загрузчика

Ваша текущая реализация имеет двухэтапную структуру загрузчика, что правильно, но в части загрузки ядра в stage2 есть несколько проблем:

Анализ Stage1

Ваш stage1 загрузчик корректно:

  • Устанавливает режим видео
  • Загружает stage2 из сектора 4 в адрес 0x7E00
  • Переходит к загруженному коду stage2

Проблемы Stage2

Проблемы находятся в секции boot stage2:

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

Коренная причина зависания

Зависание происходит потому, что stage2 загрузчик не загружает более крупное ядро корректно. Если посмотреть на ваш boot:

assembly
boot:
    mov si, load_disk
    call print
    mov dl, [BootDrive]
    xor ax, ax
    int 0x13
    jc disk_error
    mov ax, 0x1000
    mov es, ax
    mov bx, 0
    ; ... (missing disk read implementation)

Проблема в том, что вы пытаетесь загрузить ядро, но не реализовали реальную логику чтения с диска. Когда ядро небольшое (≤512 байт), это может сработать «случайно», но при большем размере система зависает, потому что:

  1. Отсутствует чтение секторов: нужно читать несколько секторов для загрузки более крупного ядра
  2. Неверное сегментное адресование: сегмент 0x1000 с смещением 0x0000 может не совпадать с ожидаемым расположением ядра
  3. Неполная настройка защищённого режима: дескрипторы GDT и инструкции перехода требуют тщательной проверки

Решения для больших ядер

Вариант 1: Загрузка ядра несколькими секторами

Самое распространённое решение – загрузка ядра несколькими секторами. Согласно codetector.org, «загрузка ядра работает так же, как загрузка второго этапа загрузчика, только нам нужно выполнять несколько вызовов BIOS для чтения диска».

Вариант 2: Улучшение Stage2 загрузчика

Stage2 загрузчик должен:

  • Читать ядро с диска сектор за сектором
  • Корректно управлять регистрами сегмента и смещения
  • Правильно обрабатывать переход в защищённый режим

Вариант 3: Поддержка файловой системы

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

Рекомендованная реализация

Ниже показано, как изменить ваш stage2 загрузчик, чтобы обрабатывать более крупные ядра:

1. Исправьте процедуру чтения диска

assembly
boot:
    mov si, load_disk
    call print
    mov dl, [BootDrive]
    
    ; Установить память для загрузки ядра (0x1000:0x0000)
    mov ax, 0x1000
    mov es, ax
    xor bx, bx          ; Смещение внутри сегмента
    mov cx, 0x0002      ; Цилиндр 0, головка 0, сектор 2
    mov al, 16          ; Количество секторов для чтения
    mov ah, 0x02        ; Функция чтения
    
    ; Чтение секторов ядра
    mov ch, 0           ; Цилиндр 0
    mov cl, 2           ; Начать с сектора 2 (после загрузчика и stage2)
    mov dh, 0           ; Головка 0
    mov dl, [BootDrive]
    int 0x13
    
    jc disk_error
    cmp al, 16          ; Сравнить прочитанные секторы с ожидаемыми
    jne disk_error
    
    ; Ядро успешно загружено по адресу 0x10000
    mov si, enter_pmode
    call print
    
    ; Настроить защищённый режим
    lidt [idtinfo]
    lgdt [gdtinfo]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp 0x08:pmode32

2. Проверьте настройку GDT

Убедитесь, что дескрипторы GDT корректно настроены для памяти вашего ядра:

assembly
gdt:    
   dd 0,0                  ; Null descriptor

codedesc:                 
   dw 0xFFFF              
   dw 0x0000              
   db 0x00                
   db 10011010b           ; Code descriptor: DPL=0, Present, 32‑bit
   db 11001111b           ; 4KB granularity, 32‑bit limit
   db 0x00                

flatdesc:                 
   dw 0xFFFF              
   dw 0x0000              
   db 0x00                
   db 10010010b           ; Data descriptor: DPL=0, Present, 32‑bit
   db 11001111b           ; 4KB granularity, 32‑bit limit
   db 0x00                

3. Полный переход в защищённый режим

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

assembly
[BITS 32]
pmode32:
    ; Настроить сегменты защищённого режима
    mov ax, 0x10           ; Flat data segment
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x9c00        ; Указатель стека
    
    ; Переход к ядру (предполагается, что ядро начинается по адресу 0x10000)
    jmp 0x08:0x10000

4. Тестируйте с постепенно увеличивающимися ядрами

Начните с небольшого ядра (512 байт) и постепенно увеличивайте размер, чтобы убедиться, что каждый шаг работает:

  1. 512 байт: должно работать с текущей настройкой
  2. 1024 байта: должно работать с загрузкой сектор за сектором
  3. 4096 байт: должно работать с правильным управлением сегментами
  4. Более крупные размеры: могут потребовать дополнительной оптимизации

Тестирование и отладка

Для отладки зависания:

  1. Добавьте отладочные выводы на каждом критическом этапе
  2. Проверьте количество секторов – убедитесь, что читается правильное число секторов
  3. Проверьте адреса памяти – убедитесь, что ядро загружено по ожидаемому адресу
  4. Тестируйте переход в защищённый режим отдельно от загрузки ядра

Ключевой вывод: ваш механизм загрузки ядра должен обрабатывать несколько секторов, а не полагаться на ограничение BIOS в 512 байт. Реализовав корректную загрузку сектор за сектором и тщательное управление памятью, ваш загрузчик сможет работать с ядрами любого размера.

Источники

  1. Bootloader - OSDev Wiki
  2. Writing a Bootloader Part 3 | Alex Parker’s Website
  3. Guide OS (2): Stage 2 and Loading Kernel | Codetector
  4. The Linux/x86 Boot Protocol — The Linux Kernel documentation
  5. Is kernel size limited to 512 bytes? - Reddit r/osdev

Заключение

Ограничение в 512 байт в вашем загрузчике – фундаментальное ограничение архитектуры x86, но оно решаемо правильной реализацией:

  1. Коренная причина: BIOS загружает только первые 512 байт, требуя многопроцессорного подхода для больших ядер
  2. Решение: реализовать загрузку ядра сектор за сектором в stage2 загрузчике
  3. Ключевые изменения: завершить процедуру чтения диска, проверить адресацию сегментов и убедиться в корректном переходе в защищённый режим
  4. Стратегия тестирования: начинать с небольших ядер и постепенно увеличивать размер, добавляя отладочный вывод

Следуя этим рекомендациям, ваш cheeseDOS загрузчик успешно справится с ядрами любого размера без зависаний во время загрузки. Ключ – корректная многосекторная загрузка и тщательное управление памятью в stage2 загрузчике.

Авторы
Проверено модерацией
Модерация