【问题标题】:Any attempt to put a string to the screen in Protected Mode causes reboot任何在保护模式下将字符串放到屏幕上的尝试都会导致重新启动
【发布时间】:2018-06-04 06:26:27
【问题描述】:

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

【问题讨论】:

  • 代码有问题,请不要存储库。
  • @Martin 对不起!我现在只能使用手机,所以我无法正确格式化它
  • 曾经安装过 IDT 吗?!现在是个好时机。这看起来像一个 三重错误,当 双重错误 处理程序 cr*ps 事情时会发生这种情况。
  • @AnttiHaapala IDT ... 中断描述符表? (根据 de.wikipedia.org,Internationale Deutschlehrertagung 也会匹配。)
  • @Scheff 当然,但我们不关心这里的德国人:)

标签: c string x86 osdev protected-mode


【解决方案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 放置在从逻辑扇区 0 开始的 1.44MiB 图像内,完成后不会截断原始文件。

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

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 可执行文件,您还可以使用符号调试。

您需要在-felf32 之后将-Fdwarf 添加到您的NASM 程序集命令以启用调试信息。将-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'

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


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

不同的偏移寻址

【讨论】:

    猜你喜欢
    • 2023-03-24
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-12
    相关资源
    最近更新 更多