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

时间:2017-12-21 14:42:53

标签: c string x86 osdev protected-mode

从头开始开发操作系统时,我刚刚进入保护模式。我已经设法进入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

1 个答案:

答案 0 :(得分: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.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被放置在1.44MiB图像内部,从逻辑扇区0开始,在完成时不截断原始文件。

已知软盘大小的正确大小的磁盘映像使其更易于在某些仿真器中使用。

A 1.44MiB floppy has 36 sectors per track(每头18个扇区,每个轨道2个头)。如果您在真实硬件上运行代码,则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可执行文件,所以您也可以使用符号调试。

您需要在-Fdwarf之后立即将-felf32添加到 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'

如果您使用Google进行搜索,有很多关于使用 GDB 的教程。有cheat sheet描述了大多数基本命令及其语法。

如果您发现自己将来遇到Interrupts,GDT或分页问题,​​我建议使用 Bochs 来调试操作系统的这些方面。虽然 Bochs 没有符号调试器,但它能够比 QEMU 更容易识别低级问题。在 Bochs 中调试实模式代码(如引导加载程序)更容易,因为它理解20位段:与 QEMU不同的偏移量寻址