Bootloader замерзает при ядре >512 байт: решение
Узнайте, почему ваш x86 bootloader замерзает, когда ядро превышает 512 байт, и как решить проблему, загружая более крупные ядра в собственном ОС для разработки.
Почему мой загрузчик зависает, когда размер ядра превышает 512 байт?
Я разрабатываю собственный x86‑загрузчик (cheeseDOS) и сталкиваюсь с проблемой: загрузчик зависает во время процесса загрузки, если размер ядра превышает 512 байт. Загрузчик успешно загружает и выполняет, когда ядро 404 байта или меньше, но зависает на шаге «Entering protected mode…» когда ядро 2,2 КБ.
Проблема возникает после добавления кода в ядро, так как возврат к предыдущей более маленькой версии ядра решает проблему. Ниже приведены мои коды stage1 и stage2 загрузчика для справки:
; 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
; 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 байт
Ограничение в 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:
- Неполная процедура чтения диска: ваш код не реализует корректно операцию чтения с диска
- Проблемы управления памятью: сегментное адресование для загрузки ядра может быть неверным
- Переход в защищённый режим: настройка GDT и переход в защищённый режим могут содержать ошибки
Коренная причина зависания
Зависание происходит потому, что stage2 загрузчик не загружает более крупное ядро корректно. Если посмотреть на ваш 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
; ... (missing disk read implementation)
Проблема в том, что вы пытаетесь загрузить ядро, но не реализовали реальную логику чтения с диска. Когда ядро небольшое (≤512 байт), это может сработать «случайно», но при большем размере система зависает, потому что:
- Отсутствует чтение секторов: нужно читать несколько секторов для загрузки более крупного ядра
- Неверное сегментное адресование: сегмент
0x1000с смещением0x0000может не совпадать с ожидаемым расположением ядра - Неполная настройка защищённого режима: дескрипторы GDT и инструкции перехода требуют тщательной проверки
Решения для больших ядер
Вариант 1: Загрузка ядра несколькими секторами
Самое распространённое решение – загрузка ядра несколькими секторами. Согласно codetector.org, «загрузка ядра работает так же, как загрузка второго этапа загрузчика, только нам нужно выполнять несколько вызовов BIOS для чтения диска».
Вариант 2: Улучшение Stage2 загрузчика
Stage2 загрузчик должен:
- Читать ядро с диска сектор за сектором
- Корректно управлять регистрами сегмента и смещения
- Правильно обрабатывать переход в защищённый режим
Вариант 3: Поддержка файловой системы
Для более продвинутых систем можно реализовать базовую поддержку файловой системы, чтобы находить и загружать файл ядра.
Рекомендованная реализация
Ниже показано, как изменить ваш stage2 загрузчик, чтобы обрабатывать более крупные ядра:
1. Исправьте процедуру чтения диска
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 корректно настроены для памяти вашего ядра:
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. Полный переход в защищённый режим
Убедитесь, что переход в защищённый режим указывает на правильный адрес:
[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 байт) и постепенно увеличивайте размер, чтобы убедиться, что каждый шаг работает:
- 512 байт: должно работать с текущей настройкой
- 1024 байта: должно работать с загрузкой сектор за сектором
- 4096 байт: должно работать с правильным управлением сегментами
- Более крупные размеры: могут потребовать дополнительной оптимизации
Тестирование и отладка
Для отладки зависания:
- Добавьте отладочные выводы на каждом критическом этапе
- Проверьте количество секторов – убедитесь, что читается правильное число секторов
- Проверьте адреса памяти – убедитесь, что ядро загружено по ожидаемому адресу
- Тестируйте переход в защищённый режим отдельно от загрузки ядра
Ключевой вывод: ваш механизм загрузки ядра должен обрабатывать несколько секторов, а не полагаться на ограничение BIOS в 512 байт. Реализовав корректную загрузку сектор за сектором и тщательное управление памятью, ваш загрузчик сможет работать с ядрами любого размера.
Источники
- Bootloader - OSDev Wiki
- Writing a Bootloader Part 3 | Alex Parker’s Website
- Guide OS (2): Stage 2 and Loading Kernel | Codetector
- The Linux/x86 Boot Protocol — The Linux Kernel documentation
- Is kernel size limited to 512 bytes? - Reddit r/osdev
Заключение
Ограничение в 512 байт в вашем загрузчике – фундаментальное ограничение архитектуры x86, но оно решаемо правильной реализацией:
- Коренная причина: BIOS загружает только первые 512 байт, требуя многопроцессорного подхода для больших ядер
- Решение: реализовать загрузку ядра сектор за сектором в stage2 загрузчике
- Ключевые изменения: завершить процедуру чтения диска, проверить адресацию сегментов и убедиться в корректном переходе в защищённый режим
- Стратегия тестирования: начинать с небольших ядер и постепенно увеличивать размер, добавляя отладочный вывод
Следуя этим рекомендациям, ваш cheeseDOS загрузчик успешно справится с ядрами любого размера без зависаний во время загрузки. Ключ – корректная многосекторная загрузка и тщательное управление памятью в stage2 загрузчике.