首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >在受保护模式下将字符串放入屏幕的任何尝试都会导致重新启动

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

Stack Overflow用户
提问于 2017-12-21 06:42:52
回答 1查看 257关注 0票数 0

我最近刚进入保护模式,当开发一个操作系统从零开始。我已经成功地进入C,并使函数打印字符到屏幕上(感谢Michael帮助我达到这个阶段)。不管怎么说,每当我尝试做一个例程,循环一个字符串文字,并打印其中的每个字符,嗯,有一点问题。QEMU只是进入一个引导循环,一次又一次地重新启动,我永远无法看到我美丽的绿色对黑色视频模式。如果我将它从一个例程中移开,并在kmain()函数中逐个打印它(我已经删除了这个部分),那么一切都很好。下面是我尝试实现字符串打印函数的文件:

vga.c -

代码语言:javascript
运行
AI代码解释
复制
#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]);
    }
}

我使用引导加载程序代码将内核读入内存:

代码语言:javascript
运行
AI代码解释
复制
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部分进行0,发出一个CLD并调用我的C代码:

代码语言:javascript
运行
AI代码解释
复制
; 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回购

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-12-21 12:17:48

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

我注意到您生成的lunaos.img大于1024字节。引导加载程序为512字节,后面的内核略大于512字节。这意味着内核现在跨越多个扇区。在kernel.asm中,使用以下代码加载单个512字节扇区:

代码语言:javascript
运行
AI代码解释
复制
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

特别是:

代码语言:javascript
运行
AI代码解释
复制
mov al, 0x01                ; read one sector (512 bytes)

这是你问题的核心。由于您是作为软盘引导,我建议生成一个1.44MiB文件,并将引导加载程序和内核放在其中:

代码语言:javascript
运行
AI代码解释
复制
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.44 per软盘每个磁道有36个扇区 (每头18个扇区,每个磁道2个磁头)。如果您在实际硬件上运行代码,一些BIOSes可能不会跨轨道边界加载。用磁盘读取35个扇区可能是安全的。第一个扇区被BIOS从0头0读取。第一轨道上还有35个行业。我会把上面的一行修改为:

代码语言:javascript
运行
AI代码解释
复制
mov al, 35                ; read 35 sectors (35*512 = 17920 bytes)

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

调试

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

您将希望将-Fdwarf添加到-felf32之后的NASM程序集命令中,以启用调试信息。在GCC命令中添加-g选项以启用调试信息。下面的命令应该启动引导程序/内核;在kmain上自动中断;使用os.elf作为调试符号;并在终端中显示源代码和寄存器。

代码语言:javascript
运行
AI代码解释
复制
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的教程。有一个备忘单描述了大多数基本命令及其语法。

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

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/47927290

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档