Как сделать ядро для моего загрузчика?

Вопрос или проблема

Я пытаюсь создать свою собственную кастомную операционную систему и мне нужна помощь с моим кодом. Вот мой bootloader.asm:

[ORG 0x7c00]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov [BOOT_DRIVE], dl
    mov bp, 0x8000
    mov sp, bp
    mov bx, 0x9000
    mov dh, 5
    mov dl, [BOOT_DRIVE]
    call load_kernel
    call enable_A20
    call graphics_mode
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp CODE_SEG:init_pm

[bits 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    jmp 0x9000

[BITS 16]
graphics_mode:
    mov ax, 0013h
    int 10h
    ret

load_kernel:
                        ; загрузить DH секторов в ES:BX с диска DL
    push dx             ; Сохранить DX в стеке, чтобы позже мы могли вспомнить,
                        ; сколько секторов было запрошено для чтения,
                        ; даже если они были изменены в это время
    mov ah , 0x02       ; функция BIOS для чтения сектора
    mov al , dh         ; Чтение DH секторов
    mov ch , 0x00       ; Выбрать цилиндр 0
    mov dh , 0x00       ; Выбрать головку 0
    mov cl , 0x02       ; Начать чтение со второго сектора (т.е.
                        ; после загрузочного сектора)
    int 0x13            ; прерывание BIOS
    jc disk_error       ; Переход при ошибке (т.е. установлен флаг переноса)
    pop dx              ; Восстановить DX из стека
    cmp dh , al         ; если AL (секторов прочитано) != DH (ожидаемых секторов)
    jne disk_error      ; вывести сообщение об ошибке
    ret
disk_error :
    mov bx , ERROR_MSG
    call print_string
    hlt

[bits 32]
    ; распечатывает строку, завершенную нулем, на которую указывает EDX
print_string :
    pusha
    mov edx , VIDEO_MEMORY ; Установить edx на начало видео памяти.
print_string_loop :
    mov al , [ ebx ] ; Хранить символ в EBX в AL
    mov ah , WHITE_ON_BLACK ; Хранить атрибуты в AH
    cmp al , 0 ; если (al == 0), конец строки, так что
    je print_string_done ; перейти к завершению
    mov [edx] , ax ; Хранить символ и атрибуты в текущей
        ; ячейке символов.
    add ebx , 1 ; Увеличить EBX до следующего символа в строке.
    add edx , 2 ; Перейти к следующей ячейке символов в видео памяти.
    jmp print_string_loop ; повторить для вывода следующего символа.
print_string_done :
    popa
    ret ; Вернуться из функции

[bits 16]
; Переменные 
ERROR_MSG db "Ошибка!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f

%include "a20.inc"
%include "gdt.inc"

times 510-($-$$) db 0
db 0x55
db 0xAA

Я компилирую это с помощью:

nasm -f bin -o boot.bin bootloader.asm

Вот kernel.c:

call_main(){main();}
void main(){}

Я компилирую это с помощью:

gcc -ffreestanding -o kernel.bin kernel.c

а затем:

cat boot.bin kernel.bin > os.bin

Я хочу знать, что я делаю не так, потому что когда я тестирую в QEMU, это не работает. Можете ли вы дать несколько советов, чтобы улучшить kernel.c, чтобы мне не нужно было использовать функцию call_main()?

При тестировании я использую:

qemu-system-i386 -kernel os.bin

Мои другие файлы

a20.inc:

   enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
   enabled:
ret

  check_a20:
pushf
push ds
push es
push di
push si

cli

xor ax, ax ; ax = 0
mov es, ax

not ax ; ax = 0xFFFF
mov ds, ax

mov di, 0x0500
mov si, 0x0510

mov al, byte [es:di]
push ax

mov al, byte [ds:si]
push ax

mov byte [es:di], 0x00
mov byte [ds:si], 0xFF

cmp byte [es:di], 0xFF

pop ax
mov byte [ds:si], al

pop ax
mov byte [es:di], al

mov ax, 0
je check_a20__exit

mov ax, 1

 check_a20__exit:
pop si
pop di
pop es
pop ds
popf

ret

    a20_bios:
mov ax, 0x2401
int 0x15
ret

    a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret

    [bits 32]
    [section .text]

    a20_keyboard:
    cli

    call    a20wait
    mov     al,0xAD
    out     0x64,al

    call    a20wait
    mov     al,0xD0
    out     0x64,al

    call    a20wait2
    in      al,0x60
    push    eax

    call    a20wait
    mov     al,0xD1
    out     0x64,al

    call    a20wait
    pop     eax
    or      al,2
    out     0x60,al

    call    a20wait
    mov     al,0xAE
    out     0x64,al

    call    a20wait
    sti
    ret

    a20wait:
    in      al,0x64
    test    al,2
    jnz     a20wait
    ret

    a20wait2:
    in      al,0x64
    test    al,1
    jz      a20wait2
    ret

gdt.inc:

 gdt_start:
dd 0                ; нулевой дескриптор--просто заполните 8 байт    dd 0 

 gdt_code:
dw 0FFFFh           ; нижний предел
dw 0                ; нижний базовый адрес
db 0                ; средний базовый адрес
db 10011010b            ; доступ
db 11001111b            ; гранулярность
db 0                ; верхний базовый адрес

 gdt_data:
dw 0FFFFh           ; нижний предел (Так же, как и код)
dw 0                ; нижний базовый адрес
db 0                ; средний базовый адрес
db 10010010b            ; доступ
db 11001111b            ; гранулярность
db 0                ; верхний базовый адрес
  end_of_gdt:

  gdtr: 
dw end_of_gdt - gdt_start - 1   ; предел (Размер GDT)
dd gdt_start            ; базовый адрес GDT

   CODE_SEG equ gdt_code - gdt_start
   DATA_SEG equ gdt_data - gdt_start

Существует ряд проблем, но в целом ваш ассемблерный код работает. Я написал ответ на StackOverflow, который содержит советы по развитию загрузчика.

Не предполагаите, что регистры сегмента настроены правильно

Исходный код в вашем вопросе не устанавливал регистр сегмента SS. Совет №1, который я даю:

Когда BIOS переходит к вашему коду, вы не можете полагаться на значения регистров CS, DS, ES, SS, SP
как на действительные или ожидаемые. Они должны быть правильно настроены,
когда ваш загрузчик начинает работу.

Если вам нужен ES, он также должен быть установлен. Хотя в вашем коде, похоже, это не так (за исключением функции print_string, о которой я расскажу позже).

Правильное определение GDT

Самая крупная ошибка, которая помешала бы вам зайти далеко в защищенном режиме, заключалась в том, что вы настроили глобальную таблицу дескрипторов (GDT) в gdt.inc, начиная с:

gdt_start:
    dd 0                ; нулевой дескриптор--просто заполните 8 байт    dd 0

Каждый глобальный дескриптор должен занимать 8 байт, но dd 0 определяет только 4 байта (двойное слово). Это должно быть:

gdt_start:
    dd 0                ; нулевой дескриптор--просто заполните 8 байт    
    dd 0

На самом деле кажется, что второй dd 0 был случайно добавлен в конец комментария на предыдущей строке.

Когда в 16-битном реальном режиме не используйте 32-битный код

Вы написали некоторый код print_string, но это 32-битный код:

[bits 32]
    ; распечатывает строку, завершенную нулем, на которую указывает EBX
print_string :
    pusha
    mov edx , VIDEO_MEMORY ; Установить edx на начало видео памяти.
print_string_loop :
    mov al , [ ebx ] ; Хранить символ в EBX в AL
    mov ah , WHITE_ON_BLACK ; Хранить атрибуты в AH
    cmp al , 0 ; если (al == 0), конец строки, так что
    je print_string_done ; перейти к завершению
    mov [edx] , ax ; Хранить символ и атрибуты в текущей
        ; ячейке символов.
    add ebx , 1 ; Увеличить EBX до следующего символа в строке.
    add edx , 2 ; Перейти к следующей ячейке символов в видео памяти.
    jmp print_string_loop ; повторить для вывода следующего символа.
print_string_done :
    popa
    ret ; Вернуться из функции

Вы вызываете print_string как обработчик ошибок в 16-битном коде, поэтому то, что вы делаете здесь, вероятно, приведет к перезагрузке компьютера. Вы не можете использовать 32-битные регистры и адресацию. Код можно сделать 16-битным с некоторыми корректировками:

    ; распечатывает строку, завершенную нулем, на которую указывает EBX
print_string :
    pusha
    push es                   ; Сохранить ES в стеке и восстановить, когда мы закончим

    push VIDEO_MEMORY_SEG     ; Сегмент видео памяти 0xb800
    pop es
    xor di, di                ; Смещение видео памяти (начать с 0)
print_string_loop :
    mov al , [ bx ] ; Хранить символ в BX в AL
    mov ah , WHITE_ON_BLACK ; Хранить атрибуты в AH
    cmp al , 0 ; если (al == 0), конец строки, так что
    je print_string_done ; перейти к завершению
    mov word [es:di], ax ; Хранить символ и атрибуты в текущей
        ; ячейке символов.
    add bx , 1 ; Увеличить BX до следующего символа в строке.
    add di , 2 ; Перейти к следующей ячейке символов в видео памяти.
    jmp print_string_loop ; повторить для вывода следующего символа.

print_string_done :
    pop es                    ; Восстановить ES, который был сохранен при входе
    popa
    ret ; Вернуться из функции

Основное различие (в 16-битном коде) заключается в том, что мы больше не используем EAX и EDX 32-битные регистры. Для того чтобы получить доступ к видео оперативной памяти @ 0xb8000, нам нужно использовать пару сегмента:смещения, которые представляют собой одно и то же. 0xb8000 можно представить как сегмент:смещение 0xb800:0x0 (вычислено как (0xb800<<4)+0x0) = 0xb8000 физический адрес. Мы можем использовать эти знания, чтобы сохранить b800 в регистре ES и использовать регистр DI как смещение для обновления видео памяти. Мы теперь используем:

mov word [es:di], ax

Чтобы переместить слово в видео оперативную память.

Сборка и линковка ядра и загрузчика

Одна из проблем, которую вы имеете при создании вашего ядра, заключается в том, что вы не правильно генерируете плоский бинарный образ, который можно загрузить непосредственно в память. Вместо того, чтобы использовать gcc -ffreestanding -o kernel.bin kernel.c, я рекомендую делать это следующим образом:

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin

Это собирает kernel.c в kernel.o с информацией отладки (-g). Затем компоновщик берет kernel.o (32-битный ELF бинарный файл) и создает исполняемый файл ELF под названием kernel.elf (этот файл будет полезен, если вы хотите отлаживать ваше ядро). Затем мы используем objcopy, чтобы взять 32-битный исполняемый файл kernel.elf и преобразовать его в плоский бинарный образ kernel.bin, который можно загрузить через BIOS. Важно отметить, что с параметром -Tlinker.ld мы просим LD (компоновщик) считывать параметры из файла linker.ld. Это простой linker.ld, который вы можете использовать для начала:

OUTPUT_FORMAT(elf32-i386)
ENTRY(main)

SECTIONS
{
    . = 0x9000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) *(COMMON) }
}

Важно отметить, что . = 0x9000 указывает компоновщику, что он должен произвести исполняемый файл, который будет загружен по адресу памяти 0x9000. 0x9000 это то место, где, как вы кажетесь, разместили ваше ядро в своем вопросе. Остальные строки делают доступными секции C, которые необходимо включить в ваше ядро, чтобы оно работало правильно.

Я рекомендую делать что-то подобное при использовании NASM, поэтому вместо использования nasm -f bin -o boot.bin bootloader.asm сделайте это следующим образом:

nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin

Это похоже на сборку C ядра. Мы не используем скрипт компоновщика здесь, но мы говорим компоновщику создавать наш код, предполагая, что код (загрузчик) будет загружен по адресу 0x7c00.

Чтобы это работало, вам нужно удалить следующую строку из bootloader.asm:

[ORG 0x7c00]

Очистка ядра (kernel.c)

Измените ваш файл kernel.c на:

/* Этот код будет помещен в начало объекта компоновщиком */    
__asm__ (".pushsection .text.start\r\n" \
         "jmp main\r\n" \
         ".popsection\r\n"
         );

/* Поместите main как первую определенную функцию в kernel.c, чтобы
 * она была в точке входа, куда наш загрузчик
 * будет вызывать. В нашем случае это будет по адресу 0x9000 */

int main(){
    /* Сделайте что-нибудь здесь*/

    return 0; /* вернуться к загрузчику */
}

В bootloader.asm мы должны вызывать функцию main (которая будет помещена по адресу 0x9000), а не переходить к ней. Вместо:

jmp 0x9000

Измените это на:

    call 0x9000
    cli
loopend:                ; Бесконечный цикл при завершении
    hlt
    jmp loopend

Код после вызова будет выполнен, когда функция C main вернется. Это простой цикл, который эффективно остановит процессор и останется в таком состоянии бесконечно, поскольку нам некуда возвращаться.

Код после внесения всех рекомендуемых изменений

bootloader.asm:

[bits 16]

global _start
_start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x8000      ; Указатель стека в SS:SP = 0x0000:0x8000
    mov [BOOT_DRIVE], dl; Загрузочный диск, переданный BIOS
    mov dh, 17          ; Количество секторов (kernel.bin), которые нужно прочитать с диска
                        ; 17*512 позволяет иметь kernel.bin до 8704 байт
    mov bx, 0x9000      ; Загрузите ядро в ES:BX = 0x0000:0x9000

    call load_kernel
    call enable_A20

;   call graphics_mode  ; Раскомментируйте, если хотите переключиться в графический режим 0x13
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp CODE_SEG:init_pm

graphics_mode:
    mov ax, 0013h
    int 10h
    ret

load_kernel:
                        ; загрузить DH секторов в ES:BX с диска DL
    push dx             ; Сохранить DX в стеке, чтобы позже мы могли вспомнить
                        ; сколько секторов было запрошено для чтения ,
                        ; даже если они были изменены в это время
    mov ah , 0x02       ; функция BIOS для чтения сектора
    mov al , dh         ; Чтение DH секторов
    mov ch , 0x00       ; Выбрать цилиндр 0
    mov dh , 0x00       ; Выбрать головку 0
    mov cl , 0x02       ; Начать чтение со второго сектора (т.е.
                        ; после загрузочного сектора )
    int 0x13            ; прерывание BIOS
    jc disk_error       ; Переход при ошибке (т.е. установлен флаг переноса )
    pop dx              ; Восстановить DX из стека
    cmp dh , al         ; если AL ( секторов прочитано ) != DH ( ожидаемых секторов )
    jne disk_error      ; вывести сообщение об ошибке
    ret
disk_error :
    mov bx , ERROR_MSG
    call print_string
    hlt

; распечатывает строку, завершенную нулем, на которую указывает EDX
print_string :
    pusha
    push es                   ; Сохранить ES в стеке и восстановить, когда мы закончим

    push VIDEO_MEMORY_SEG     ; Сегмент видео памяти 0xb800
    pop es
    xor di, di                ; Смещение видео памяти (начать с 0)
print_string_loop :
    mov al , [ bx ] ; Хранить символ в BX в AL
    mov ah , WHITE_ON_BLACK ; Хранить атрибуты в AH
    cmp al , 0 ; если (al == 0), конец строки, так что
    je print_string_done ; перейти к завершению
    mov word [es:di], ax ; Хранить символ и атрибуты в текущей
        ; ячейке символов.
    add bx , 1 ; Увеличить BX до следующего символа в строке.
    add di , 2 ; Перейти к следующей ячейке символов в видео памяти.
    jmp print_string_loop ; повторить для вывода следующего символа.

print_string_done :
    pop es                    ; Восстановить ES, который был сохранен при входе
    popa
    ret ; Вернуться из функции

%include "a20.inc"
%include "gdt.inc"

[bits 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    call 0x9000
    cli
loopend:                                ; Бесконечный цикл при завершении
    hlt
    jmp loopend

[bits 16]
; Переменные
ERROR            db "Ошибка A20!" , 0
ERROR_MSG        db "Ошибка!" , 0
BOOT_DRIVE:      db 0

VIDEO_MEMORY_SEG equ 0xb800
WHITE_ON_BLACK   equ 0x0f

times 510-($-$$) db 0
db 0x55
db 0xAA

gdt.inc:

gdt_start:
    dd 0                ; нулевой дескриптор--просто заполните 8 байт
    dd 0

gdt_code:
    dw 0FFFFh           ; нижний предел
    dw 0                ; нижний базовый адрес
    db 0                ; средний базовый адрес
    db 10011010b        ; доступ
    db 11001111b        ; гранулярность
    db 0                ; верхний базовый адрес

gdt_data:
    dw 0FFFFh           ; нижний предел (Так же, как и код)
    dw 0                ; нижний базовый адрес
    db 0                ; средний базовый адрес
    db 10010010b        ; доступ
    db 11001111b        ; гранулярность
    db 0                ; верхний базовый адрес
end_of_gdt:

gdtr:
    dw end_of_gdt - gdt_start - 1   ; предел (Размер GDT)
    dd gdt_start        ; базовый адрес GDT

    CODE_SEG equ gdt_code - gdt_start
    DATA_SEG equ gdt_data - gdt_start

a20.inc:

enable_A20:
    call check_a20
    cmp ax, 1
    je enabled
    call a20_bios
    call check_a20
    cmp ax, 1
    je enabled
    call a20_keyboard
    call check_a20
    cmp ax, 1
    je enabled
    call a20_fast
    call check_a20
    cmp ax, 1
    je enabled
    mov bx, [ERROR]
    call print_string
enabled:
    ret

check_a20:
    pushf
    push ds
    push es
    push di
    push si

    cli
    xor ax, ax ; ax = 0
    mov es, ax
    not ax ; ax = 0xFFFF
    mov ds, ax
    mov di, 0x0500
    mov si, 0x0510
    mov al, byte [es:di]
    push ax
    mov al, byte [ds:si]
    push ax
    mov byte [es:di], 0x00
    mov byte [ds:si], 0xFF
    cmp byte [es:di], 0xFF
    pop ax
    mov byte [ds:si], al
    pop ax
    mov byte [es:di], al
    mov ax, 0
    je check_a20__exit
    mov ax, 1

check_a20__exit:
    pop si
    pop di
    pop es
    pop ds
    popf
    ret

a20_bios:
    mov ax, 0x2401
    int 0x15
    ret

a20_fast:
    in al, 0x92
    or al, 2
    out 0x92, al
    ret

    [bits 32]
    [section .text]

a20_keyboard:
    cli

    call    a20wait
    mov     al,0xAD
    out     0x64,al
    call    a20wait
    mov     al,0xD0
    out     0x64,al
    call    a20wait2
    in      al,0x60
    push    eax
    call    a20wait
    mov     al,0xD1
    out     0x64,al
    call    a20wait
    pop     eax
    or      al,2
    out     0x60,al
    call    a20wait
    mov     al,0xAE
    out     0x64,al
    call    a20wait
    sti
    ret

a20wait:
    in      al,0x64
    test    al,2
    jnz     a20wait
    ret

a20wait2:
    in      al,0x64
    test    al,1
    jz      a20wait2
    ret

kernel.c:

/* Этот код будет помещен в начало объекта компоновщиком */    
__asm__ (".pushsection .text.start\r\n" \
         "jmp main\r\n" \
         ".popsection\r\n"
         );

/* Поместите main как первую определенную функцию в kernel.c, чтобы
 * она была в точке входа, куда наш загрузчик
 * будет вызывать. В нашем случае это будет по адресу 0x9000 */

int main(){
    /* Сделайте что-нибудь здесь*/

    return 0; /* вернуться к загрузчику */
}

linker.ld

OUTPUT_FORMAT(elf32-i386)
ENTRY(main)

SECTIONS
{
    . = 0x9000;
    .text : { *(.text.start) *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) *(COMMON) }
}

Создание образа диска с помощью DD / Отладка с QEMU

Если вы используете приведенные выше файлы и создаете необходимые загрузчик и ядро с помощью этих команд (как упоминалось ранее)

nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin

gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin

Вы можете создать образ диска (в данном случае мы сделаем его размером с дискета) с помощью этих команд:

dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc

Это создает заполненный нулями образ диска размером 512*2880 байт (Размер 1,44 мегабайта дискетки). dd if=boot.bin of=disk.img bs=512 conv=notrunc записывает boot.bin в первый сектор файла, не обрезая образ диска. dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc помещает kernel.bin в образ диска, начиная со второго сектора. Параметр seek=1 пропускает первый блок (bs=512) перед записью.

Если вы хотите запустить свое ядро, вы можете запустить его как диск A: (-fda) в QEMU следующим образом:

qemu-system-i386 -fda disk.img

Вы также можете отлаживать свое 32-битное ядро, используя QEMU и GNU Debugger (GDB) с информацией о отладке, которую мы сгенерировали при компиляции/сборке кода с помощью приведенных выше инструкций.

qemu-system-i386 -fda disk.img -S -s &
gdb kernel.elf  \
        -ex 'target remote localhost:1234' \
        -ex 'layout src' \
        -ex 'layout reg' \
        -ex 'break main' \
        -ex 'continue'

Этот пример запускает QEMU с удаленным отладчиком и эмулирует дискеты, используя файл disk.img (который мы создали с помощью DD). GDB запускается, используя kernel.elf (файл, который мы сгенерировали с информацией отладки), затем подключается к QEMU и устанавливает точку останова в функции main в коде C. Когда отладчик готов, вам будет предложено нажать <return> для продолжения. При удаче вы должны видеть функцию main в отладчике.

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

Ответ или решение

Создание собственного загрузчика и ядра для кастомной ОС — это захватывающая, но сложная задача. Ваша реализация, судя по предоставленному коду, имеет несколько важных моментов, которые мы можем улучшить, чтобы добиться успешной загрузки вашей системы. Я опишу основные проблемы вашего кода и внесу предлагаемые изменения для успешного старта вашего ядра.

Проблемы в коде загрузчика

  1. Инициализация сегментов и стеков:
    Вы правильно делаете, инициализируя сегменты в начале вашего загрузчика, однако не следует полагаться на то, что регистры CS, DS, ES и SS будут установлены должным образом BIOS. Вы должны убедиться, что все сегменты правильно заданы.

  2. Определение таблицы глобальных дескрипторов (GDT):
    В вашем файле gdt.inc есть ошибка в определении дескрипторов, где вы неправильно задаете размер. Каждый дескриптор в GDT должен занимать 8 байт.

  3. Выбор правильного кода для режимов работы:
    Вы неправильно смешиваете 16-битный и 32-битный код. Загрузка произойдет в реальном режиме, поэтому все вызовы, которые вы делаете в этом режиме, должны основываться на 16-битных регистрах.

  4. Правильный алгоритм компиляции ядра:
    Ваше ядро не генерирует правильный «плоский» бинарный образ, который требуется для загрузки. Я дам вам правильные команды для компиляции и линковки вашего ядра.

Как исправить?

Изменения в файлах

1. Загрузчик (bootloader.asm)

Ниже приведен исправленный код загрузчика:

[bits 16]
global _start
_start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x8000
    mov [BOOT_DRIVE], dl
    mov dh, 17
    mov bx, 0x9000
    call load_kernel
    call enable_A20
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
    jmp CODE_SEG:init_pm

[bits 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov esp, 0x90000
    call 0x9000
    cli
.loop:
    hlt
    jmp .loop

load_kernel:
    push dx
    mov ah, 0x02
    mov al, dh
    mov ch, 0x00
    mov dh, 0x00
    mov cl, 0x02
    int 0x13
    jc disk_error
    pop dx
    cmp dh, al
    jne disk_error
    ret

disk_error:
    mov bx, ERROR_MSG
    call print_string
    hlt

print_string:
    pusha
    ; здесь ваш код для отображения строки на экране
    ; Используйте 16-битные регистры вместо 32-битных.
    popa
    ret

ERROR_MSG db "Error!", 0
BOOT_DRIVE db 0

%include "gdt.inc"
%include "a20.inc"
times 510 - ($ - $$) db 0
db 0x55
db 0xAA

2. Ядро (kernel.c)

Модифицированный код вашего ядра:

extern void _start(); // Указываем на функцию старта в ассемблере

void main() {
    // Ваш код, например:
    // Здесь вы можете выполнять некоторые задачи или оставить бесконечный цикл
    while (1) {}
}

// Вставьте подпрограмму, которая должна быть вызвана в bootloader
void _start() {
    // Ваша логика, которая выполняется при старте
}

Правильная команды для компиляции и линковки

Вместо просто компиляции с помощью GCC, используйте следующий метод:

Для загрузчика:

nasm -f elf32 -o boot.o bootloader.asm
gcc -m32 -c -ffreestanding -o kernel.o kernel.c
ld -Ttext 0x7C00 --oformat binary -o boot.bin boot.o

Для ядра:

gcc -m32 -c -ffreestanding -o kernel.o kernel.c
ld -Ttext 0x9000 -Tstatic --oformat binary -o kernel.bin kernel.o

Создание образа диска

Для того чтобы протестировать в QEMU, вы можете создать образ диска:

dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc

Запуск в QEMU

Теперь вы можете протестировать ваш загрузчик и ядро в QEMU:

qemu-system-i386 -hda disk.img

Заключение

С приведенными изменениями вы должны быть в состоянии запустить свою ОС без проблем в QEMU. Убедитесь, что вы внимательно следите за всеми сегментами и регистровыми операциями, так как это ключ к правильной загрузке и выполнению вашего ядра. Удачи в ваших разработках, и если у вас есть дополнительные вопросы, не стесняйтесь задавать их!

Оцените материал
Добавить комментарий

Капча загружается...