任何在保护模式下将字符串放到屏幕上的尝试都会导致重启

Any attempt to put a string to the screen in Protected Mode causes reboot

我最近在从头开发 OS 时进入了保护模式。我已经设法进入 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 repo

TL;DR:您还没有在 start.asm 中使用引导加载程序将整个内核读入内存。缺少代码 and/or 数据导致您的内核因三重故障而崩溃,从而导致重新启动。随着内核的增长,您将需要读取更多扇区。


我注意到您生成的 lunaos.img 大于 1024 字节。 bootloader 是 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.44MiB 文件并将您的引导加载程序和内核放入其中:

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.44MiB 文件。第二个使用 conv=notrunc 告诉 DD 在写入后不要截断文件。 seek=0 告诉 DD 从文件的第一个逻辑扇区开始写入。结果将是 os.bin 被放置在从逻辑扇区 0 开始的 1.44MiB 图像中,完成后不会截断原始文件。

已知软盘大小的适当大小的磁盘映像使其更易于在某些模拟器中使用。

A 1.44MiB floppy has 36 sectors per track (18 sectors per head, 2 heads per track). If you run your code on real hardware, some BIOSes 可能无法跨轨道边界加载。您可能会安全地读取磁盘读取的 35 个扇区。第一个扇区由 BIOS 在磁道 0 磁头 0 之外读取。在第一磁道上还有 35 个扇区。我将上面的行修改为:

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

这将使您的内核长度为 35*512 字节 = 17920 字节,即使在真实硬件上也不会遇到麻烦。任何比这更大的,你将不得不考虑用一个尝试读取多个轨道的循环来修改你的引导加载程序。更复杂的是,您不得不担心更大的内核最终会超过 64k 段限制。可能必须修改磁盘读取以使用不是 0 的段 (ES)。如果您的内核变得那么大,您的引导加载程序可以在那时修复。


调试

由于您处于保护模式并使用 QEMU,我强烈建议您考虑使用调试器。 QEMU 支持使用 GDB 进行远程调试。设置起来并不困难,因为您已经生成了内核的 ELF 可执行文件,您还可以使用符号调试。

您需要在 -felf32 之后立即将 -Fdwarf 添加到 NASM 汇编命令中以启用调试信息。将 -g 选项添加到 GCC 命令以启用调试信息。下面的命令应该启动你的 bootloader/kernel;在 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有很多教程。有一个 cheat sheet 描述了大多数基本命令及其语法。


如果您以后发现自己在中断、GDT 或分页方面遇到问题,我建议您使用 Bochs 来调试操作系统的这些方面。尽管 Bochs 没有符号调试器,但它弥补了比 QEMU 更容易识别低级问题的能力。在 Bochs 中调试像引导加载程序这样的实模式代码更容易,因为它理解 20 位 segment:offset 寻址不像 QEMU