为什么我的代码不能在屏幕上绘制白色像素?

SKZ*_*KZI 1 x86 assembly gcc nasm osdev

我正在制作一个运行 Snake 的操作系统,并且我处于 32 位保护模式,但我无法在屏幕上绘制像素。我在实模式下切换到模式 0x13 (320x200x256),屏幕一片空白。进入保护模式后,内核运行并且我正在绘制的像素不会出现。

我从这篇OSDev 文章中汲取灵感,该文章切换到保护模式并将像素绘制到视频显示上。

这是kernel.c

#include "display.h"    // Include Display Drivers
#include "constants.h" // Include constants that C doesn't have included
#include "keyboard.h" // Include the custom keyboard driver

void _start(){} // Remove LD Warning

DisplayDetails globalDisplayDetails; // The display details.

int main(){
    // Init the display details
    DisplayDetails details = display_init();
    globalDisplayDetails = details;
    bool running = true;
    
    while(running){
        // This is the OS loop
        putpixel(100, 100, 0x00FFFFFF);
    }
    
    // return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是display.h(实际的显示驱动程序)

#define VRAM 0xA0000

typedef struct DisplayModeDetails {
    int width;            // Width of one line of pixels
    int height;          //  Height of display in pixels
    int colors;         //   How many colors are supported
    int pitch;         //    How many bytes of VRAM to skip when going 1 pixel down
    int pixelWidth;   //     How many bytes of VRAM to skip when going 1 pixel right
} DisplayDetails;

struct DisplayModeDetails display_init(){
    struct DisplayModeDetails details; // Setup display mode details
    details.width      = 640;
    details.height     = 480;
    details.colors     = 16;
    details.pitch      = 1;
    details.pixelWidth = 1;
    return details;
}

void putpixel(int pos_x, int pos_y, unsigned long VGA_COLOR)
{
    unsigned char* location = (unsigned char*)0xA0000 + 320 * pos_y + pos_x;
    *location = VGA_COLOR;
}
Run Code Online (Sandbox Code Playgroud)

boot_sect.asm

[bits 16]
[org 0x7C00]

mov [BOOTDRIVE], dl

; call clear_screen ; Clear screen
call load_kernel

load_kernel:
    mov bx, KERNELOFFSET
    mov dh, 0x05
    mov dl, [BOOTDRIVE]
    ; mov dl, 0x80
    call clear_screen     ; Clear Screen
    call disk_load        ; Load From Disk

    mov ah, 0x00          ; Start setting video mode
    mov al, 0x13          ; 320x200 256 color graphics
    int 0x10

    cli                   ; Disable Interrupts
    lgdt [gdt_descriptor] ; GDT start address
    mov eax, cr0
    or eax, 1
    mov cr0, eax          ; Jump to Protected 32 bit mode
    jmp CODESEG:start_protected_mode

    jmp $

clear_screen:
    pusha

    mov ah, 0x07     ; Scroll al lines; 0 = all
    mov bh, 0x0f     ; white on black
    mov cx, 0x00     ; row=0, col=0
    mov dx, 0x184f   ; row = 24, col = 79
    int 0x10         ; Call interrupt

    mov ah, 0x02
    mov dh, 0x00
    mov dl, 0x00
    mov bh, 0x00
    int 0x10

    popa
    ret

disk_load:
    pusha
    push dx
    mov ah, 0x02 ; read mode
    mov al, dh   ; read dh number of sects
    mov cl, 0x02 ; read from sect 2 (1 = boot)
    mov ch, 0x00 ; cylinder 0
    mov dh, 0x00 ; head 0

    int 0x13
    jc disk_error

    pop dx
    cmp al, dh

    jne sectors_error
    popa
    ret

disk_error:
    mov ah, '1' ; Error Code
    jmp err_loop
sectors_error:
    mov ah, '2' ; Error Code
    jmp err_loop
err_loop:
    call clear_screen
    mov dh, ah ; Print Error Message
    mov ah, 0x0e
    mov al, 'E'
    int 0x10
    mov al, 'r'
    int 0x10
    int 0x10
    mov al, ' '
    int 0x10
    mov al, dh ; Print Error Code
    int 0x10
    
    jmp $ ; create infinite loop

; Constants
KERNELOFFSET equ 0x1000
CODESEG equ gdt_code - gdt_start
DATASEG equ gdt_data - gdt_start

gdt_start:
    dq 0x0

    gdt_null:
        dd 0x0
        dd 0x0

    gdt_code:
        dw 0xffff
        dw 0x0
        db 0x0
        db 0b10011010
        db 0b11001111
        db 0x0
    
    gdt_data:
        dw 0xffff
        dw 0x0
        db 0x0
        db 0b10010010
        db 0b11001111
        db 0x0
    gdt_end:

    gdt_descriptor:
        dw gdt_end - gdt_start
        dd gdt_start

[bits 32]
start_protected_mode:
    ; Load the kernel
    mov ax, DATASEG
    mov dx, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    
    mov ebp, 0x9000
    mov esp, ebp

    call KERNELOFFSET
    jmp $

BOOTDRIVE db 0

; Marking as bootable.
times 510-($-$$) db 0
dw 0xaa55
Run Code Online (Sandbox Code Playgroud)

kernel_entry.asm

[bits 32]
[extern main]
call main
jmp $
Run Code Online (Sandbox Code Playgroud)

我用这个脚本构建和测试:

gcc -m32 -fno-pie -ffreestanding -g -c kernel.c  -o obj/main.o

nasm -f elf kernel_entry.asm -o obj/entry.o
nasm -f bin boot_sect.asm -o obj/boot.bin

ld -m elf_i386 -o obj/kernel.bin -Ttext 0x1000 obj/main.o obj/entry.o  --oformat binary

cat obj/boot.bin obj/kernel.bin > bin/os.bin

qemu-system-x86_64 -drive format=raw,file=bin/os.bin -display sdl
Run Code Online (Sandbox Code Playgroud)

pax*_*blo 8

首先,模式 13 是每个像素一个字节,所以我不确定您为什么要使用它,0x00ffffff除非您认为它是 RGB/RGBA 值,但该模式并非如此。

当将此值作为单个字节推入内存时,它将变为0xff,根据模式 13 维基百科页面是使用的调色板:

在此输入图像描述

在我看来,0xff(右下)和0x00(左上)一样黑。因此,我怀疑如果你想要白色,你应该使用0x0f(右上角)。将是我要尝试的第一件事。

除此之外,您可能需要确保在 32 位平面模式(或同等模式)下运行,其中当前使用的选择器基于物理地址零并且足够大以到达视频内存,即正常的 32 位平面模式 4G应该可以解决问题:-)

那是因为您使用的起始位置为A0000。自从我进行这种级别的图形编程以来已经有一段时间了,足够长的时间我使用段寄存器而不是选择器,因此将寄存器设置A000为零并将偏移量设置为零。但我确实记得您需要正确解释逻辑地址如何变成物理地址。


进一步研究一下底层操作系统代码,假设您按照链接使用此处的代码,我注意到的一件事是 GDT 的设置方式。数据段条目为:

gdt_data:
    dw 0xffff        # Segment limit b0-15  = ffff.
    dw 0x0           # Segment base b0-15   = 0000.
    db 0x0           # Segment base b16-23  = 00.
    db 0b10010010
    db 0b11001111    # Segment limit b16-19 = f, granularity 4K.
    db 0x0           # Segment base b24-31  = 00
Run Code Online (Sandbox Code Playgroud)

这意味着您的数据选择器的0x00000000限制为0xfffff(由于粒度是 4K 而不是单个字节,因此这是整个 4G 空间)。

因此,为了到达物理地址A0000,您将使用(正如您所拥有的那样)0xA0000-那里似乎没有问题。但是,我确实注意到该代码中其他地方有一行奇怪的行:

start_protected_mode:
    ; Load the kernel
    mov ax, DATASEG
    mov dx, ax          ;; <<-- this one.
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
Run Code Online (Sandbox Code Playgroud)

此代码段似乎正在设置所有非 CS 选择器,以便它们使用您的数据段,这是预期的。但是,上面的第二个mov将选择器移至dx而不是ds,并且似乎没有其他代码可以修改ds

根据OSDev x86 系统初始化页面

当 BIOS 将控制权转移到引导扇区时,关于系统状态的标准化内容很少。唯一(几乎)确定的是引导扇区代码已加载并在物理地址运行0x7c00,CPU 处于 16 位实模式,称为 DL 的 CPU 寄存器包含“驱动器号”,并且只有 512 字节引导扇区已加载。

那里没有提到ds寄存器内容,而且我见过的大多数引导代码都没有做出任何假设,而是显式地设置了它需要的一切。

如果ds没有引用正确的选择器,则逻辑到物理的映射可能无法工作,并且写入A0000将到达预期之外的位置(或由于选择器无效而出现故障)。所以这条线可能应该设置ds而不是dx


并且,为了完整起见,将Michael Petch提出的其他问题纳入评论中:

您的构建还存在另一个严重问题。确保在链接时首先obj/entry.o列出,以便代码正确启动内核(请参见下面的第二行):

ld -m elf_i386 -o obj/kernel.bin -Ttext 0x1000
    obj/entry.o obj/main.o
    --oformat binary
Run Code Online (Sandbox Code Playgroud)

此外,由于 x86 和 x86-64 软件模拟在用户模式下运行时可能会跳过某些检查,例如超出段限制,因此设置不ds正确qemu可能不会陷入困境。

但是,使用该--enable-kvm选项或在完整系统模式(当然,或者在实际硬件上)运行它可能会出现错误。

  • @SKZI:虽然您的 Github 存储库不是最新代码,但 boot_sect.asm 中存在拼写错误。`mov ax, DATASEG` `mov dx, ax` 应该是 `mov ax, DATASEG` `mov ds, ax`。您的构建还存在另一个严重问题。确保链接时首先列出“obj/entry.o”,以便代码正确启动内核。所以 `ld -m elf_i386 -o obj/kernel.bin -Ttext 0x1000 obj/main.o obj/entry.o --oformat binary` 应该是 `ld -m elf_i386 -o obj/kernel.bin -Ttext 0x1000 obj/ Entry.o obj/main.o --oformat 二进制` 。这是帕克斯对颜色的评论之上的。 (3认同)
  • 谢谢,@MichaelPetch,也将其添加到答案中,*并且*正确地归因于您的添加(由于我剪切和粘贴了错误的链接而错误地归因于OP)。 (2认同)