Search code examples
assemblygccx86nasmosdev

Why won't my code draw a white pixel to the screen?


I'm making an OS that runs snake, and I'm in 32 bit protected mode, But I can't draw a pixel to the screen. I switch to Mode 0x13 (320x200x256) in real mode and the screen blanks. After entering protected mode the kernel runs and the pixel I am plotting doesn't appear.

I am drawing inspiration from this OSDev Article that switches to protected mode and plots pixels to the video display.

Here's 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;
}

And Here's display.h (The actual display driver)

#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;
}

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

kernel_entry.asm:

[bits 32]
[extern main]
call main
jmp $

I build and test with this script:

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

Solution

  • First up, mode 13 is one byte per pixel, so I'm not sure why you're using 0x00ffffff unless you think it's an RGB/RGBA value, which is not the case for this mode.

    When pushing this value into memory as a single byte, it will become 0xff and, according to the Mode 13 Wikipedia page, this is the color palette used:

    enter image description here

    It looks to me that 0xff (bottom right) is as black as 0x00 (top left). Hence I suspect you should be using 0x0f (top right) if you want white. That would be the first thing I'd try.

    Beyond that, you probably need to ensure you're running in 32-bit flat mode (or equivalent), where the current selector used is based at physical address zero and large enough to reach video memory, the normal 32-bit flat mode 4G should do the trick :-)

    That's because you're using an starting location of A0000. It's been a while since I did this level of graphics programming, long enough that I was using segment registers rather than selectors, hence had the register set to A000 and based the offsets at zero. But I do remember you needed to properly account for how logical addresses became physical ones.


    Investigating the underlying OS code a little more, assuming you're using the code here as per your link, one thing I notice is the way the GDT is set up. The data segment entry is:

    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
    

    That means your data selector is based at 0x00000000 with a limit of 0xfffff (since granularity is 4K rather than a single byte, this is the entire 4G space).

    So, in order to get to physical address A0000, you would use (as you have) 0xA0000 - there appears to be no problem there. However, I do notice one strange line elsewhere in that code:

    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
    

    This code segment appears to be setting up all the non-CS selectors so that they use your data segment, which is as expected. However, the second mov above moves the selector into dx rather than ds, and there's no other code that appears to modify ds.

    As per the OSDev x86 system initialisation page,

    There are very few things that are standardized about the state of the system, when the BIOS transfers control to the bootsector. The only things that are (nearly) certain are that the bootsector code is loaded and running at physical address 0x7c00, the CPU is in 16-bit Real Mode, the CPU register called DL contains the "drive number", and that only 512 bytes of the bootsector have been loaded.

    No mention is made there of the ds register content and most boot code I've seen makes no assumptions, explicitly setting everything it needs.

    If ds is not referencing the correct selector, the logical-to-physical mapping may not work, and writing to A0000 will go somewhere other than intended (or fault because the selector is invalid). So the line should probably set ds rather than dx.


    And, just for completeness, incorporating other issues raised by Michael Petch in the comments:

    There is another serious problem with your build. Make sure when linking that obj/entry.o is listed first so that the code properly starts your kernel (see second line below):

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

    Additionally, it's possible that not setting ds correctly didn't get caught in qemu as x86 and x86-64 software emulation may skip certain checks when running in user mode, such as exceeding segment limits.

    However, running it with the --enable-kvm option, or in full system mode (or, of course, on actual hardware), may have seen a fault raised.