Aneesh Sharma Aneesh Sharma - 12 days ago 7
C Question

Unexpected output when printing directly to text video memory

I am developing a kernel in C and created something to print on screen on video memory. I expected that the first byte in video memory would be the character to print and the second byte tells the color. But my program has something different but it works!! It is very unexpected and unusual.

My kernel code -

#define VIDEO_MEM 0xb8000

void write_string( int colour, const unsigned char *string );

void main()
{
unsigned char *vid = (unsigned char*) VIDEO_MEM;
int i=0;
for (i = 0; i < 2000; i++)
{
*vid = ' ';
*(vid+2) = 0x1f;
vid += 2;
}
write_string(0x1f,"The Kernel has been loaded successfully!!");
}

void write_string( int colour, const unsigned char *string ) {
unsigned char *vid = (unsigned char*) VIDEO_MEM;
while(*string != 0)
{
*(vid) = *string;
*(vid+2) = colour;
++string;
vid+=2;
}
}


It prints the character on
*vid
and the color on
*(vid+2)
and then increments the
vid
by 2. It should then replace and print the next char on
*(vid+2)
. So, the color should go but it still works.

Also, the color should be on
*(vid+1)


When I use
*(vid+1)
instead of
*(vid+2)
to print the string, the screen shows down arrow characters (with ACII code
0x1f
which I wanted to be the color) replacing the entire string.

Why does the code behave so unusual??

Can anyone help?

EDIT

I have edited my code and now it prints string. But another problem arose. I added a support for printing on particular line number. But now this shifts the string backwards by one character.

void write_string( int colour, const unsigned char *string, int pos ) {
unsigned char *vid = (unsigned char*) VIDEO_MEM;
vid+=pos*160;
while(*string != 0)
{
*vid = colour;
*(vid+1) = *string;
++string;
vid+=2;
}

}


So, If I tell it to print on line 10, it prints the first character on the last character of the 9th line and then continues.

I also have a character printing function that justs prints curly braces (
}
) instead of the given character and that too one character backwards of the given position (like the error in the
write_string
function). Also it doen't change the character background color given as argument.

void putChar(char character, short col, short row, char attr) {
unsigned char* vid_mem = (unsigned char *) VIDEO_MEM;
int offset = (row*80 + col)*2;
vid_mem += offset;
if(!attr) {
attr = 0x0f;
}
*vid_mem = (attr<<8)+character;
}





EDIT 2

My Boot Loader:

[org 0x7c00]

KERNEL equ 0x1000

mov [BOOT_DRIVE],dl

mov bp,0x9000
mov sp,bp

mov bx, msgReal
call print_string

call load_kernel

call switch_to_pm

jmp $

%include 'boot/bios.ASM'

%include 'boot/gdt.ASM'
%include 'boot/protected_mode.ASM'
%include 'boot/print32.ASM'

[bits 16]
load_kernel:
mov bx,msgKernel
call print_string

mov bx, KERNEL
mov dh, 15
mov dl, [BOOT_DRIVE]
call disk_load
ret

[bits 32]

BEGIN_PM:
mov ebx, msgProt
call print_string32
call KERNEL
jmp $

BOOT_DRIVE db 0
msgReal db "Booted in 16-bit mode",0
msgProt db "Successfully switched to 32-bit mode",0
msgKernel db "Loading the kernel onto memory",0

times 510-($-$$) db 0
dw 0xaa55


bios.ASM -

;BIOS Functions
[bits 16]

print_string:
pusha
mov cx,bx
mov ah,0x0e
printStringStart:
mov al,[bx]
cmp al,0
je done
int 0x10
inc bx
jmp printStringStart
done:
popa
ret

print_word:
pusha
mov ax,0x0000
mov cl,0x10
mov al,bh
div cl
call printDig
mov al,bh
and al,0x0f
call printDig
mov ax,0x0000
mov al,bl
div cl
call printDig
mov al,bl
and al,0x0f
call printDig
popa
ret

printDig:
cmp al,0x9
jg alpha
add al,'0'
mov ah,0x0e
int 0x10
jmp pDigDone
alpha:
sub al,0xa
add al,'A'
mov ah,0x0e
int 0x10
pDigDone:
ret

hex_prefix: db '0x',0

disk_load:
push dx
mov ah,0x02
mov al,dh
mov ch,0x00
mov dh,0x00
mov cl,0x02
int 0x13
jc disk_error
pop dx
cmp dh,al
jne disk_error
ret

disk_error:
mov ah,0x0e
mov al,'X'
int 0x10
mov bx,errMsg
call print_string
jmp $

errMsg:
db "Disk Read Error....."
times 80-20 db " "
db 0


gdt.ASM -

gdt_start:
gdt_null:
dd 0x0
dd 0x0

gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0

gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:

gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start


protected_mode.ASM -

[bits 16]
switch_to_pm:
cli
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp CODE_SEG:init_pm

[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax

mov ebp,0x90000
mov esp,0x90000

call BEGIN_PM


print32.ASM -

[bits 32]

VIDEO_MEM equ 0xb8000
DEF_COLOR equ 0x0f

print_string32:
pusha
mov edx,VIDEO_MEM

print_string32_loop:
mov al, [ebx]
mov ah, DEF_COLOR

cmp al,0
je print_string32_end

mov [edx],ax

inc ebx
add edx,2
jmp print_string32_loop

print_string32_end:
popa
ret


I also add a kernel_start.asm file just before the kernel while linking to call the main function -

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


And here's my make file -

C_SOURCES = $(wildcard drivers/*.c kernel/*.c)
HEADERS = $(wildcard kernel/*.h drivers/*.h)

OBJ = ${C_SOURCES:.c=.o}

all: os-image

os-image: boot/boot_sector.bin kernel.bin
cat $^ > $@

kernel.bin: kernel/kernel_start.o ${OBJ}
ld -o $@ -Ttext 0x1000 $^ --oformat binary

%.o : %.c
gcc -std=c99 -Wall -pedantic -ffreestanding -c $< -o $@

%.o : %.asm
nasm $< -f elf64 -o $@

%.bin : %.asm
nasm $< -f bin -o $@

clean:
rm -fr kernel/*.o
rm -fr drivers/*.o
rm -fr boot/*.bin
rm -fr os-image *.bin *.o

Answer

With the changes suggested in other answer and comments, your problem doesn't seem to be reproducible for me. The following code works for me. I've tried to maintain how you coded it just so it makes sense to you:

#define VIDEO_MEM 0xb8000

void write_string( unsigned char colour, const char *string );
void write_string_line( unsigned char colour, const char *string, int pos );
void putChar(char character, short col, short row, unsigned char attr);

/* Place this at top of file as first code in kernel.o */
__asm__ ("call main\r\n" \
         "cli\r\n" \
         "hlt\r\n"
         );

void main()
{
    volatile unsigned char *vid = (unsigned char*) VIDEO_MEM;
    int i=0;
    for (i = 0; i < 2000; i++)
    {
        *vid = ' ';
        *(vid+1) = 0x1f;
        vid += 2;
    }
    write_string(0x1f,"The Kernel has been loaded successfully!!");
    write_string_line(0x1f,"Testing Here!!",1);
    putChar('Z',3,3,0xf3);
}

void write_string( unsigned char colour, const char *string ) {
    volatile unsigned char *vid = (unsigned char*) VIDEO_MEM;
    while(*string != 0)
    {
        *(vid) = *string;
        *(vid+1) = colour;
        ++string;
        vid+=2;
    }
}

void write_string_line( unsigned char colour, const char *string, int pos ) {
    volatile unsigned char *vid = (unsigned char*) VIDEO_MEM;
    vid+=pos*160;
    while(*string != 0)
    {
        *vid = *string;
        *(vid+1) = colour;
        ++string;
        vid+=2;
    }

}

void putChar(char character, short col, short row, unsigned char attr) {
    volatile unsigned char* vid_mem = (unsigned char *) VIDEO_MEM;
    int offset = (row*80 + col)*2;
    vid_mem += offset;
    if(!attr) {
        attr = 0x0f;
    }
    *(unsigned short int *)vid_mem = (attr<<8)+character;
    /* This would do the same as line above
    *vid_mem     = character;
    *(vid_mem+1) = attr;
    */
}

I've added the __asm__ at the beginning to make sure that code is the first to appear in the generated object file. It likely works without it. I've modified all your *vid pointers to be volatile . Since video is memory mapped IO you don't want to have the compiler potentially remove screen writes when it optimizes. Likely your code will work without volatile, but it is proper to add it here to avoid potential problems.

When run BOCHS this code produces this screen output:

enter image description here

If you use the code provided here and it doesn't work that would suggest the issue you are having is likely related to the a code you write in your bootloader that read the disk, enabled A20, set the GDT, entered protected mode, and then called into your C code. It is also possible problems could occur depending on how you compile and link your kernel.


Likely Cause of Undefined Behavior

After all the code and the make file were made available in EDIT 2 it became clear that one significant problem was that most of the code was compiled and linked to 64-bit objects and executables. That code won't work in 32-bit protected mode.

In the make file make these adjustments:

  • When compiling with GCC you need to add -m32 option
  • When assembling with GNU Assembler (as) targeting 32-bit objects you need to use --32
  • When linking with LD you need to add the -melf_i386 option
  • When assembling with NASM targeting 32-bit objects you need to change -f elf64 to -f elf32
Comments