Любая попытка вывести строку на экран в защищенном режиме вызывает перезагрузку

Недавно я перешел в защищенный режим при разработке ОС с нуля. Мне удалось перейти на C и создать функции для вывода символов на экран (спасибо Майклу Петчу за помощь в достижении этого этапа). В любом случае, всякий раз, когда я пытаюсь создать подпрограмму, которая перебирает строковый литерал и печатает в нем каждый символ, возникает небольшая проблема. QEMU просто переходит в загрузочный цикл, перезагружается снова и снова, и я никогда не вижу свой прекрасный видеорежим зеленого на черном. Если я вынесу это из подпрограммы и распечатаю посимвольно в функции kmain() (той части, которую я удалил), все будет работать нормально и денди. Вот файл, в котором я пытаюсь реализовать функцию печати строк:

vga.c -

#include <vga.h>

size_t terminal_row;
size_t terminal_column;
uint8_t terminal_color;
uint16_t *terminal_buffer;

volatile uint16_t * const VIDMEM = (volatile uint16_t *) 0xB8000;

size_t strlen(const char *s)
{
    size_t len = 0;
    while(s[len]) {
        len++;
    }
    return len;
}

void terminal_init(void) 
{
    terminal_row = 0;
    terminal_column = 0;
    terminal_color = vga_entry_color(LGREEN, BLACK);
    for(size_t y = 0; y < VGA_HEIGHT; y++) {
        for(size_t x = 0; x < VGA_WIDTH; x++) {
            const size_t index = y * VGA_WIDTH + x;
            VIDMEM[index] = vga_entry(' ', terminal_color);
        } 
    }
}

void terminal_putentryat(char c, uint8_t color, size_t x, size_t y)
{
    const size_t index = y * VGA_WIDTH + x;
    VIDMEM[index] = vga_entry(c, color);
}

void terminal_putchar(char c)
{
    terminal_putentryat(c, terminal_color, terminal_column, terminal_row);
    if(++terminal_column == VGA_WIDTH) {
        terminal_column = 0;
        if(++terminal_row == VGA_HEIGHT) {
            terminal_row = 0;
        }
    }
}

void terminal_puts(const char *s)
{
    size_t n = strlen(s);
    for (size_t i=0; i < n; i++) {
        terminal_putchar(s[i]);
    }
}

Я прочитал свое ядро ​​​​в память с помощью этого кода загрузчика:

extern kernel_start             ; External label for start of kernel
global boot_start               ; Make this global to suppress linker warning
bits 16

boot_start:
    xor ax, ax                  ; Set DS to 0. xor register to itselfzeroes register
    mov ds, ax
    mov ss, ax                  ; Stack just below bootloader SS:SP=0x0000:0x7c00
    mov sp, 0x7c00

    mov ah, 0x00
    mov al, 0x03
    int 0x10

load_kernel:
    mov ah, 0x02                ; call function 0x02 of int 13h (read sectors)
    mov al, 0x01                ; read one sector (512 bytes)
    mov ch, 0x00                ; track 0
    mov cl, 0x02                ; sector 2
    mov dh, 0x00                ; head 0
;    mov dl, 0x00               ; drive 0, floppy 1. Comment out DL passed to bootloader
    xor bx, bx                  ; segment 0x0000
    mov es, bx                  ; segments must be loaded from non immediate data
    mov bx, 0x7E00              ; load the kernel right after the bootloader in memory 
.readsector:
    int 13h                     ; call int 13h
    jc .readsector              ; error? try again

    jmp 0x0000:kernel_start     ; jump to the kernel at 0x0000:0x7e00

У меня есть заглушка сборки в начале моего ядра, которая входит в защищенный режим, обнуляет раздел BSS, выдает CLD и вызывает мой код C:

; These symbols are defined by the linker. We use them to zero BSS section
extern __bss_start
extern __bss_sizel

; Export kernel entry point
global kernel_start

; This is the C entry point defined in kmain.c
extern kmain               ; kmain is C entry point
bits 16

section .text
kernel_start:

    cli    

    in al, 0x92
    or al, 2
    out 0x92, al

    lgdt[toc]

    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp 0x08:start32     ; The FAR JMP is simplified since our segment is 0

section .rodata
gdt32:
    dd 0
    dd 0

    dw 0x0FFFF
    dw 0
    db 0
    db 0x9A
    db 0xCF
    db 0

    dw 0x0FFFF
    dw 0
    db 0
    db 0x92
    db 0xCF
    db 0
gdt_end:
toc:
    dw gdt_end - gdt32 - 1
    dd gdt32             ; The GDT base is simplified since our segment is now 0

bits 32
section .text
start32:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x9c000    ; Set the stack to grow down from area under BDA/Video memory

    ; We need to zero out the BSS section. We'll do it a DWORD at a time
    cld
    lea edi, [__bss_start] ; Start address of BSS
    lea ecx, [__bss_sizel] ; Lenght of BSS in DWORDS
    xor eax, eax           ; Set to 0x00000000
    rep stosd              ; Do clear using string store instruction

    call kmain

У меня есть специальный скрипт компоновщика, который размещает загрузчик по адресу 0x7c00, а ядро ​​— по адресу 0x7e00.

В чем проблема и как я могу это исправить? Я сделал свой репозиторий git доступным, если потребуется дополнительная информация.


person Safal Aryal    schedule 21.12.2017    source источник
comment
Код, о котором идет речь, не репозиторий, пожалуйста.   -  person Martin James    schedule 21.12.2017
comment
@Мартин, прости! Сейчас у меня есть доступ только к телефону, поэтому я не могу его правильно отформатировать.   -  person Safal Aryal    schedule 21.12.2017
comment
Вы уже когда-либо устанавливали IDT?! сейчас будет хорошее время. Это похоже на тройную ошибку, которая возникает, когда обработчик двойной ошибки что-то портит.   -  person Antti Haapala    schedule 21.12.2017
comment
@AnttiHaapala IDT ... Таблица дескрипторов прерываний? (Internationale Deutschlehrertagung также соответствует de.wikipedia.org.)   -  person Scheff's Cat    schedule 21.12.2017
comment
@Scheff конечно, но нас здесь не волнуют немцы :)   -  person Martin James    schedule 21.12.2017
comment
На несвязанной ноте. section .bss resb 33 в нижней части файла kernel.asm можно удалить. Он был там для целей тестирования, и я забыл удалить его перед представлением.   -  person Michael Petch    schedule 22.12.2017


Ответы (1)


TL;DR: вы не прочитали все ядро ​​​​в память с помощью загрузчика в start.asm. Отсутствующий код и/или данные вызывают сбой ядра с тройной ошибкой, что приводит к перезагрузке. Вам нужно будет читать больше секторов по мере роста вашего ядра.


Я заметил, что ваш сгенерированный lunaos.img больше 1024 байт. Загрузчик 512 байт, а ядро ​​после него чуть больше 512 байт. Это означает, что ядро ​​теперь охватывает несколько секторов. В вашем kernel.asm вы загружаете один 512-байтовый сектор с помощью этого кода:

load_kernel:
    mov ah, 0x02                ; call function 0x02 of int 13h (read sectors)
    mov al, 0x18                ; read one sector (512 bytes)
    mov ch, 0x00                ; track 0
    mov cl, 0x02                ; sector 2
    mov dh, 0x00                ; head 0
;    mov dl, 0x00               ; drive 0, floppy 1. Comment out DL passed to bootloader
    xor bx, bx                  ; segment 0x0000
    mov es, bx                  ; segments must be loaded from non immediate data
    mov bx, 0x7E00              ; load the kernel right after the bootloader in memory
.readsector:
    int 13h                     ; call int 13h
    jc .readsector              ; error? try again

Особенно:

mov al, 0x01                ; read one sector (512 bytes)

В этом суть вашей проблемы. Поскольку вы загружаетесь с дискеты, я бы рекомендовал создать файл размером 1,44 МБ и поместить в него загрузчик и ядро ​​с помощью:

dd if=/dev/zero of=bin/lunaos.img bs=1024 count=1440
dd if=bin/os.bin of=bin/lunaos.img bs=512 conv=notrunc seek=0

Первая команда создает файл размером 1,44 МБ, заполненный нулями. Второй использует conv=notrunc, чтобы указать DD не обрезать файл после записи. seek=0 указывает DD начать запись с первого логического сектора в файле. В результате os.bin помещается внутрь образа размером 1,44 МБ, начиная с логического сектора 0, без усечения исходного файла по завершении.

Образ диска с известным размером гибкого диска правильного размера упрощает его использование в некоторых эмуляторах.

дискета объемом 1,44 МБ имеет 36 секторов на дорожку (18 секторов на головку, 2 головки на отслеживать). Если вы запускаете свой код на реальном оборудовании, некоторые BIOS могут не загружаться через граница дорожки. Вероятно, вы безопасно читаете 35 секторов при чтении диска. Первый сектор был прочитан биосом off track 0 head 0. На первой дорожке есть еще 35 секторов. Я бы изменил строку выше, чтобы быть:

mov al, 35                ; read 35 sectors (35*512 = 17920 bytes)

Это позволит вашему ядру иметь длину 35 * 512 байт = 17920 байт с минимальными проблемами даже на реальном оборудовании. Если больше, чем это, вам придется рассмотреть возможность модификации вашего загрузчика циклом, который пытается прочитать более одной дорожки. Чтобы усложнить ситуацию, вам придется беспокоиться о том, что более крупные ядра в конечном итоге превысят лимит сегментов в 64 КБ. Чтение диска, вероятно, должно быть изменено, чтобы использовать сегмент (ES), который не равен 0. Если ваше ядро ​​становится таким большим, ваш загрузчик может быть исправлен в это время.


Отладка

Поскольку вы находитесь в защищенном режиме и используете QEMU, я настоятельно рекомендую вам рассмотреть возможность использования отладчика. QEMU поддерживает удаленную отладку с помощью GDB. Его несложно настроить, и поскольку вы сгенерировали исполняемый файл ELF своего ядра, вы также можете использовать символьную отладку.

Вам нужно будет добавить -Fdwarf к командам сборки NASM сразу после -felf32, чтобы включить отладочную информацию. Добавьте параметр -g в команды GCC, чтобы включить отладочную информацию. Команда ниже должна запустить ваш загрузчик/ядро; автоматически прерываться на kmain; используйте os.elfдля символов отладки; и отобразить исходный код и регистры в терминале.

qemu-system-i386 -fda bin/lunaos.img -S -s &

gdb bin/os.elf \
        -ex 'target remote localhost:1234' \
        -ex 'layout src' \
        -ex 'layout regs' \
        -ex 'break *kmain' \
        -ex 'continue'

Существует множество руководств по использованию GDB, если вы выполняете поиск в Google. Существует шпаргалка, в которой описывается большинство основных команд и их синтаксис.


Если в будущем у вас возникнут проблемы с прерываниями, GDT или пейджингом, я рекомендую использовать Bochs для отладки этих аспектов операционной системы. Хотя Bochs не имеет символьного отладчика, он компенсирует это тем, что может легче идентифицировать низкоуровневые проблемы, чем QEMU. Отладка кода реального режима, такого как загрузчики, проще в Bochs, учитывая, что он понимает 20-битную адресацию segment:offset, в отличие от QEMU

person Michael Petch    schedule 21.12.2017