Why is my video memory offset calculation off by one?
Asked Answered
P

1

6

I have been reading and following a tutorial on writing an operating system from scratch by Nick Blundell which can be found at https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf

I have successfully written a boot loader that can call C code, and so I have started writing my kernel in C. I am now trying to write functions that can print characters and strings on screen. By the time I start executing C code, I am in 32-bit protected mode, so I am trying to correctly calculate memory offsets from video memory address 0xb8000.

My problem starts when I try to access specific regions of video memory using a calculated offset. Since the text area is 25 rows by 80 columns, I use the formula ((row * 80) + column) * 2 since I have to have a character byte and an attribute byte. When I set row = 0 and column = 0, the X I'm trying to print is absent. Setting row = 0 and column = 1, an X appears at the upper left hand corner.

Starting with char* video_memory = 0xb8000 and repeatedly issuing video_memory++ allows me to correctly visit each byte and print a space on a black background.

This is my main code:

#include "../drivers/screen.h"

void main() {

   //clear_screen();
   //print_character('X', 0, 0, 0);

   // Helper variables.
   int row;
   int column;

   // We need to point at 0xB8000, where video memory resides.
   unsigned char* video_memory = (unsigned char*)0xB8000;
   for(row = 0; row < 25; row++) {
      for(column = 0; column < 80; column++) {
         // Clear the screen by printing a space on a black background.
         *video_memory = ' ';
         video_memory += 1;
         *video_memory = 0x0F;
         video_memory += 1;
      }
   }

   // Test the offset calculation by printing at row 0, column 0 (the upper 
   // left corner of the screen).
   row = 0;
   column = 0;

   // For an 80 by 25 grid. Multiply by 2 to account for the need of two bytes 
   // to display a character with given attributes.
   int offset = ((row * 80) + column) * 2;

   // Reset memory location after the loop.
   video_memory = (unsigned char*)0xB8000;

   // Add the offset to get the desired cell.
   // THIS IS WHERE THE PROBLEM IS! Setting column = 1 prints in the first cell
   // of video memory instead of the second.
   video_memory += offset;

   // Set character and its attributes.
   *video_memory = 'X';
   video_memory++;
   *video_memory = 0x0F;
}

This is the console displayed when row = 0 and column = 0: The console when row and column are 0. No X appears.

This is the console when row = 0 and column = 1: The console when row is 0 and column is 1. An X appears.

This is an objdump of my kernel.c file above:

kernel.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
#include "../drivers/screen.h"

void main() {
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   // Helper variables.
   int row;
   int column;

   // We need to point at 0xB8000, where video memory resides.
   unsigned char* video_memory = (unsigned char*)0xB8000;
   4:   48 c7 45 f8 00 80 0b    mov    QWORD PTR [rbp-0x8],0xb8000
   b:   00 
   for(row = 0; row < 25; row++) {
   c:   c7 45 ec 00 00 00 00    mov    DWORD PTR [rbp-0x14],0x0
  13:   eb 2f                   jmp    44 <main+0x44>
      for(column = 0; column < 80; column++) {
  15:   c7 45 f0 00 00 00 00    mov    DWORD PTR [rbp-0x10],0x0
  1c:   eb 1c                   jmp    3a <main+0x3a>
         // Clear the screen by printing a space on a black background.
         *video_memory = ' ';
  1e:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  22:   c6 00 20                mov    BYTE PTR [rax],0x20
         video_memory += 1;
  25:   48 83 45 f8 01          add    QWORD PTR [rbp-0x8],0x1
         *video_memory = 0x0F;
  2a:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  2e:   c6 00 0f                mov    BYTE PTR [rax],0xf
         video_memory += 1;
  31:   48 83 45 f8 01          add    QWORD PTR [rbp-0x8],0x1
   int column;

   // We need to point at 0xB8000, where video memory resides.
   unsigned char* video_memory = (unsigned char*)0xB8000;
   for(row = 0; row < 25; row++) {
      for(column = 0; column < 80; column++) {
  36:   83 45 f0 01             add    DWORD PTR [rbp-0x10],0x1
  3a:   83 7d f0 4f             cmp    DWORD PTR [rbp-0x10],0x4f
  3e:   7e de                   jle    1e <main+0x1e>
   int row;
   int column;

   // We need to point at 0xB8000, where video memory resides.
   unsigned char* video_memory = (unsigned char*)0xB8000;
   for(row = 0; row < 25; row++) {
  40:   83 45 ec 01             add    DWORD PTR [rbp-0x14],0x1
  44:   83 7d ec 18             cmp    DWORD PTR [rbp-0x14],0x18
  48:   7e cb                   jle    15 <main+0x15>
      }
   }

   // Test the offset calculation by printing at row 0, column 0 (the upper 
   // left corner of the screen).
   row = 0;
  4a:   c7 45 ec 00 00 00 00    mov    DWORD PTR [rbp-0x14],0x0
   column = 0;
  51:   c7 45 f0 00 00 00 00    mov    DWORD PTR [rbp-0x10],0x0

   // For an 80 by 25 grid. Multiply by 2 to account for the need of two bytes 
   // to display a character with given attributes.
   int offset = ((row * 80) + column) * 2;
  58:   8b 55 ec                mov    edx,DWORD PTR [rbp-0x14]
  5b:   89 d0                   mov    eax,edx
  5d:   c1 e0 02                shl    eax,0x2
  60:   01 d0                   add    eax,edx
  62:   c1 e0 04                shl    eax,0x4
  65:   89 c2                   mov    edx,eax
  67:   8b 45 f0                mov    eax,DWORD PTR [rbp-0x10]
  6a:   01 d0                   add    eax,edx
  6c:   01 c0                   add    eax,eax
  6e:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax

   // Reset memory location after the loop.
   video_memory = (unsigned char*)0xB8000;
  71:   48 c7 45 f8 00 80 0b    mov    QWORD PTR [rbp-0x8],0xb8000
  78:   00 

   // Add the offset to get the desired cell.
   // THIS IS WHERE THE PROBLEM IS! Setting column = 1 prints in the first cell
   // of video memory instead of the second.
   video_memory += offset;
  79:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  7c:   48 98                   cdqe   
  7e:   48 01 45 f8             add    QWORD PTR [rbp-0x8],rax

   // Set character and its attributes.
   *video_memory = 'X';
  82:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  86:   c6 00 58                mov    BYTE PTR [rax],0x58
   video_memory++;
  89:   48 83 45 f8 01          add    QWORD PTR [rbp-0x8],0x1
   *video_memory = 0x0F;
  8e:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  92:   c6 00 0f                mov    BYTE PTR [rax],0xf
}
  95:   90                      nop
  96:   5d                      pop    rbp
  97:   c3                      ret    

I have traced over and hand checked the actual assembly instructions for my offset calculation, and they appear to be correct. I suspect the issue arises when I try to add my offset (type int) to my video memory address (type unsigned char*), but again I'm not entirely sure.

Also, I tried hard coding specific numbers for offsets. Using video_memory += 0 instead of video_memory += offset works as desired.

Profitsharing answered 22/5, 2018 at 4:0 Comment(7)
Unrelated to your problem, but don't modify video_memory and where it points. Use normal array indexing instead. As in video_memory[row * 80 + column] = 'X';Felicefelicia
It's interesting that you mention that. My first attempts used array indexing, but for whatever reason, the results were never what I expected, hence the switch to direct modification. I do intend to go back to array indexing after I get this particular problem sorted out.Profitsharing
What byte value does the debugger indicate is actually occupying 0xB8000 and 0xB80001 after you write an 'X' and 0x0F in the final steps of this code?Ulent
Have you tried writing other values? Other characters? Other attribtues? To other locations? To 0xB8000, 0xB8001, 0xB8002, 0xB8003? The idea is to establish a pattern. Maybe your calculation is not off by one, it could be the video memory being off by one because the first byte is e.g. a global attribute.Acquirement
@Ulent After running through Bochs until my screen is 'cleared', Bochs reports a value of 0x0f200f20.Profitsharing
@Acquirement Yes. In the first double loop, I replaced the attribute byte with 0xAF to give a green background, and I replaced the space with the @ symbol. Every square is colored green with a white @ symbol. Normally on startup, every square in the Bochs emulator has a black background. This makes me think that the first byte is being used to display a character with attributes, instead of as some sort of global attribute as you suggested.Profitsharing
@Yunnosch, one more thing, Setting video_memory = 0xB8001 after the doubly-nested loop (all else the same) results in the first cell having a magenta back ground with a space, and the second cell now has a black background with a white *.Profitsharing
P
3

After more searching, I found an article on the ARM Information Center describing the use of C pointers to access specific addresses for memory mapped I/O devices.

Declaring my video_memory pointer variable as 'volatile' ensures that "the compiler always carries out the memory-accesses, rather than optimizing them out...". Apparently, according to this answer on Quora, compilers can generate instructions that overwrite data in a write buffer before that data is flushed to memory, which is where my problem occurs.

So declaring my variable as volatile unsigned char* video_memory = 0xB8000; produces the expected results.

Profitsharing answered 23/5, 2018 at 2:19 Comment(4)
How did that influence the generated assembly code by the compiler? The assembly you posted in your original question doesn't seem to imply anything getting optimized out. Would love to know what changed.Ulent
@selbie, sorry it took so long to reply. After using an online difference checker, it appears that the only differences occur where I declare my variable in the first place. All of the assembly instructions are the same.Profitsharing
Doesn't make much sense if the volatile keyword didn't change the assembly. Perhaps the bug where the buffer was getting overwritten was occurring somewhere else, but the volatile keyword was still protecting.Ulent
Yeah, it's weird. Every source I check states that volatile is used to make sure memory references are checked instead of cached away. The main example I see is that a hardware component can change the values of it's IO registers, which would be beyond my control.Profitsharing

© 2022 - 2024 — McMap. All rights reserved.