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

Я только недавно перешел в защищенный режим при разработке ОС с нуля. Мне удалось войти в C и создать функции для вывода символов на экран (спасибо Michael Petch за помощь в достижении этой стадии). В любом случае, всякий раз, когда я пытаюсь создать подпрограмму, которая перебирает строковый литерал и печатает каждый символ в нем, что ж, есть небольшая проблема. 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-репо доступным, если нужно больше информации.

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.44MiB дискета имеет 36 секторов на дорожку (18 секторов на голову, 2 головки на дорожку). Если вы выполняете свой код на реальном оборудовании, некоторые BIOS могут не загружаться через границы дорожки. Скорее всего, вы читаете 35 секторов на вашем диске. Первый сектор был прочитан BIOS с нулевой дорожки 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-битный сегмент: адресация смещения в отличие от QEMU

Другие вопросы по тегам