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

Я пытаюсь создать свою собственную ОС, и мне нужна помощь с моим кодом. Это мой 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: ; load DH sectors to ES:BX from drive DL push dx ; Store DX on stack so later we can recall ; how many sectors were request to be read , ; even if it is altered in the meantime mov ah , 0x02 ; BIOS read sector function mov al , dh ; Read DH sectors mov ch , 0x00 ; Select cylinder 0 mov dh , 0x00 ; Select head 0 mov cl , 0x02 ; Start reading from second sector ( ie ; after the boot sector ) int 0x13 ; BIOS interrupt jc disk_error ; Jump if error ( ie carry flag set ) pop dx ; Restore DX from the stack cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected ) jne disk_error ; display error message ret disk_error : mov bx , ERROR_MSG call print_string hlt [bits 32] ; prints a null - terminated string pointed to by EDX print_string : pusha mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem. print_string_loop : mov al , [ ebx ] ; Store the char at EBX in AL mov ah , WHITE_ON_BLACK ; Store the attributes in AH cmp al , 0 ; if (al == 0) , at end of string , so je print_string_done ; jump to done mov [edx] , ax ; Store char and attributes at current ; character cell. add ebx , 1 ; Increment EBX to the next char in string. add edx , 2 ; Move to next character cell in vid mem. jmp print_string_loop ; loop around to print the next char. print_string_done : popa ret ; Return from the function [bits 16] ; Variables ERROR_MSG db "Error!" , 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 ; null descriptor--just fill 8 bytes dd 0 gdt_code: dw 0FFFFh ; limit low dw 0 ; base low db 0 ; base middle db 10011010b ; access db 11001111b ; granularity db 0 ; base high gdt_data: dw 0FFFFh ; limit low (Same as code) dw 0 ; base low db 0 ; base middle db 10010010b ; access db 11001111b ; granularity db 0 ; base high end_of_gdt: gdtr: dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT) dd gdt_start ; base of 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 ; null descriptor--just fill 8 bytes dd 0 

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

     gdt_start: dd 0 ; null descriptor--just fill 8 bytes dd 0 

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

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

    Вы написали код print_string но это 32-разрядный код:

     [bits 32] ; prints a null - terminated string pointed to by EBX print_string : pusha mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem. print_string_loop : mov al , [ ebx ] ; Store the char at EBX in AL mov ah , WHITE_ON_BLACK ; Store the attributes in AH cmp al , 0 ; if (al == 0) , at end of string , so je print_string_done ; jump to done mov [edx] , ax ; Store char and attributes at current ; character cell. add ebx , 1 ; Increment EBX to the next char in string. add edx , 2 ; Move to next character cell in vid mem. jmp print_string_loop ; loop around to print the next char. print_string_done : popa ret ; Return from the function 

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

      ; prints a null - terminated string pointed to by EBX print_string : pusha push es ;Save ES on stack and restore when we finish push VIDEO_MEMORY_SEG ;Video mem segment 0xb800 pop es xor di, di ;Video mem offset (start at 0) print_string_loop : mov al , [ bx ] ; Store the char at BX in AL mov ah , WHITE_ON_BLACK ; Store the attributes in AH cmp al , 0 ; if (al == 0) , at end of string , so je print_string_done ; jump to done mov word [es:di], ax ; Store char and attributes at current ; character cell. add bx , 1 ; Increment BX to the next char in string. add di , 2 ; Move to next character cell in vid mem. jmp print_string_loop ; loop around to print the next char. print_string_done : pop es ;Restore ES that was saved on entry popa ret ; Return from the function 

    Основное различие (в 16-битном коде) заключается в том, что мы больше не используем 32-разрядные регистры EAX и EDX . Чтобы получить доступ к видеопамяти @ 0xb8000, нам нужно использовать пару сегментов: offset, которая представляет одно и то же. 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 (этот файл будет полезен, если вы хотите отлаживать ваше kernel). Затем мы используем objcopy, чтобы взять исполняемый файл ELF32 kernel.elf и преобразовать его в плоское двоичное изображение kernel.bin, которое может быть загружено BIOS. -Tlinker.ld отметить, что с параметром -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 . 0x9000 – это то место, где вы, похоже, поместили свое kernel ​​в свой вопрос. Остальные строки предоставляют разделы C , которые необходимо будет включить в ваше kernel ​​для правильной работы.

    Я рекомендую делать что-то подобное при использовании 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 следующим образом:

     /* This code will be placed at the beginning of the object by the linker script */ __asm__ (".pushsection .text.start\r\n" \ "jmp main\r\n" \ ".popsection\r\n" ); /* Place main as the first function defined in kernel.c so * that it will be at the entry point where our bootloader * will call. In our case it will be at 0x9000 */ int main(){ /* Do Stuff Here*/ return 0; /* return back to bootloader */ } 

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

     jmp 0x9000 

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

      call 0x9000 cli loopend: ;Infinite loop when finished hlt jmp loopend 

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

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

    bootloader.asm :

     [bits 16] global _start _start: cli xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x8000 ; Stack pointer at SS:SP = 0x0000:0x8000 mov [BOOT_DRIVE], dl; Boot drive passed to us by the BIOS mov dh, 17 ; Number of sectors (kernel.bin) to read from disk ; 17*512 allows for a kernel.bin up to 8704 bytes mov bx, 0x9000 ; Load Kernel to ES:BX = 0x0000:0x9000 call load_kernel call enable_A20 ; call graphics_mode ; Uncomment if you want to switch to 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: ; load DH sectors to ES:BX from drive DL push dx ; Store DX on stack so later we can recall ; how many sectors were request to be read , ; even if it is altered in the meantime mov ah , 0x02 ; BIOS read sector function mov al , dh ; Read DH sectors mov ch , 0x00 ; Select cylinder 0 mov dh , 0x00 ; Select head 0 mov cl , 0x02 ; Start reading from second sector ( ie ; after the boot sector ) int 0x13 ; BIOS interrupt jc disk_error ; Jump if error ( ie carry flag set ) pop dx ; Restore DX from the stack cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected ) jne disk_error ; display error message ret disk_error : mov bx , ERROR_MSG call print_string hlt ; prints a null - terminated string pointed to by EDX print_string : pusha push es ;Save ES on stack and restore when we finish push VIDEO_MEMORY_SEG ;Video mem segment 0xb800 pop es xor di, di ;Video mem offset (start at 0) print_string_loop : mov al , [ bx ] ; Store the char at BX in AL mov ah , WHITE_ON_BLACK ; Store the attributes in AH cmp al , 0 ; if (al == 0) , at end of string , so je print_string_done ; jump to done mov word [es:di], ax ; Store char and attributes at current ; character cell. add bx , 1 ; Increment BX to the next char in string. add di , 2 ; Move to next character cell in vid mem. jmp print_string_loop ; loop around to print the next char. print_string_done : pop es ;Restore ES that was saved on entry popa ret ; Return from the function %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: ;Infinite loop when finished hlt jmp loopend [bits 16] ; Variables ERROR db "A20 Error!" , 0 ERROR_MSG db "Error!" , 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 ; null descriptor--just fill 8 bytes dd 0 gdt_code: dw 0FFFFh ; limit low dw 0 ; base low db 0 ; base middle db 10011010b ; access db 11001111b ; granularity db 0 ; base high gdt_data: dw 0FFFFh ; limit low (Same as code) dw 0 ; base low db 0 ; base middle db 10010010b ; access db 11001111b ; granularity db 0 ; base high end_of_gdt: gdtr: dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT) dd gdt_start ; base of 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 :

     /* This code will be placed at the beginning of the object by the linker script */ __asm__ (".pushsection .text.start\r\n" \ "jmp main\r\n" \ ".popsection\r\n" ); /* Place main as the first function defined in kernel.c so * that it will be at the entry point where our bootloader * will call. In our case it will be at 0x9000 */ int main(){ /* Do Stuff Here*/ return 0; /* return back to bootloader */ } 

    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) перед записью.

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

     qemu-system-i386 -fda disk.img 

    Вы также можете отлаживать свое 32-битное kernel ​​с помощью 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. Когда окончательный отладчик будет готов, вам будет предложено нажать для продолжения. В любом случае вы должны просматривать функцию main в отладчике.

    Interesting Posts
    Давайте будем гением компьютера.