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

Я пытаюсь создать свою собственную ОС, и мне нужна помощь с моим кодом. Это мой 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 ( i.e.
                        ; after the boot sector )
    int 0x13            ; BIOS interrupt
    jc disk_error       ; Jump if error ( i.e. 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

Ответ 1

Существует ряд проблем, но в целом ваш код сборки работает. Я написал ответ 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(этот файл будет полезен, если вы хотите отлаживать ваше ядро). Затем мы используем objcopy, чтобы взять исполняемый файл ELF32 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. Мы не используем компоновщик script здесь, но мы говорим компоновщику, чтобы произвести наш код, предполагая, что код (загрузчик) будет загружен в 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 ( i.e.
                        ; after the boot sector )
    int 0x13            ; BIOS interrupt
    jc disk_error       ; Jump if error ( i.e. 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).

Если вы хотите запустить ядро, вы можете запустить его в качестве флоппи-дисковода 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 в отладчике.