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
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:
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.