Write Your Own OS (5) — Print to screen

Meg's tech corner
5 min readJul 4, 2020

Previous: Write Your Own OS (4) — Boot Process [https://medium.com/@megtechcorner/write-your-own-os-4-boot-process-b7cb7bef2fcb]

Next: Write Your Own OS (6) — Control the cursor (Coming soon)

— — — — — — — — — — — — — — — — — — — — —

Part 1.2 Print to screen

In this section, we will extend our Operating System to print to the screen and move the cursor to the appropriate location. First we will set up a stack for the Operating System so that we can switch to the C programming language. Next we will print to the screen using framebuffer. Lastly we will control the location of the cursor with I/O instructions.

The starter code for this tutorial can be found at [https://github.com/megstechcorner/meg-os/tree/part-1.1-bare-bone-os].

The completed code for this tutorial can be found at [https://github.com/megstechcorner/meg-os/tree/part-1.2-print-to-screen].

Switching to C

So far, we have been using assembly programming language. It works however it can be tedious to program in assembly. So we will switch to a programming language with richer syntax and easier to program and debug. Any higher-level programming language such as Java and C++ would work here. However we use C in this tutorial. The reason is that C has comparatively simple syntax and readers from other programming language backgrounds should be able to pick it up quickly. C is also powerful enough to meet our needs.

To use C, there is one more setup we need to do, a stack. Function calls implementation in C requires the existence of a stack. Let’s use the following code as an example to illustrate how a stack is used in C. [Note stack here is different from a data structure called stack.]

A stack is nothing but a continuous chuck of memory. For the Operating System, we will reserve some physical memory for its stack at initialization time. For user programs, the Operating System will allocate free memory for their stacks. For x86 machines, the stack grows towards lower addresses. As shown in the figure below, the top of the stack has a smaller address than the bottom of the stack. It is normally the program’s responsibility to make sure it doesn’t use the memory outside the stack, otherwise it can corrupt memory or cause StackOverflow error.

The figure on the left shows the state of stack before calling bar() inside foo() function. The ESP register holds the address of the top of foo()’s stack, esp(1). All the local variables are inside the stack.

The figure on the right shows the state of stack after bar() is called. Another stack frame is allocated for function bar(). The ESP register now has the address of the top of bar()’s stack, esp(2). The local variables of bar() are stored in the stack as well.

After the function is returned from bar(), the ESP register again holds the value of the top of foo()’s stack, esp(1).

To access local variables, the CPU checks the value inside the ESP register to figure out what is the stack to use. It then fetches the value for the local variable from that stack. Therefore before calling bar() and after returning from bar(), ESP register holds the value esp(1). The CPU will therefore fetch the value for the local variable from foo()’s stack. However when we are inside the bar() function, the ESP register has value esp(2). The CPU will correctly fetch values from the stack of bar(). The CPU can therefore disambiguate local variables for different functions.

To set up a stack is straightforward. In the start.S, we will make use of the following line of code to reserve 4KB memory in the BSS section.

.lcomm kernel_stack, KERNEL_STACK_SIZE # Reserve 4kb kernel stack

After that, we only need to make the ESP register to hold the value of the bottom of the stack. Now we are ready to switch to the C programming language. [main function is defined in the kmain.c file.]

start.S

Print to screen

The content displayed on our computer screen is driven by a memory device called framebuffer. The first 8 bits inside the framebuffer represent the ASCII value for the character to be displayed at the position (row_0, column_0). The next 4 bits encode the foreground color of the text while the next 4 bits are for the background color of the text. A mapping from color to value is shown below.

To sum up, 16 bits are used to represent a character to be displayed. The first 16 bits are for position (row_0, column_1). The next 16 bits are for the position (row_0, column_1) and so on.

To enable access to the framebuffer, the framebuffer is usually memory-mapped to address 0xb8000. We will discuss memory-mapping in more details in later sections. The high-level idea is that when we write to the memory at address 0xb8000, instead of writing the data to physical memory, the data is written to the framebuffer at address 0.

A simple example to print “hi” at the top left corner of the display is shown in the figure above. We define a 3d array, fb, with starting address 0xB8000. fb[i][j][0] represents the character at position (row_i, column_j) while fb[i][j][1] represents the attribute of that character.

fb[0][0][0] for instance has address 0xB8000 and therefore is written to the address 0 of framebuffer (first 8 bit). fb[0][0][1] is written to the next 8 bit. Framebuffer then updates the screen upon receiving the data.

Compiling and running the Operating System, we will be able to see “hi” at the top left corner.

> make

> qemu-system-x86_64 -kernel kernel.elf

However, there are at least two issues with the current code

  • Cursor position is not correct
  • It is tedious to manipulate the content of the screen

We will address both issues in the next tutorial.

— — — — — — — — — — — — — — — — — — — — —

Previous: Write Your Own OS (4) — Boot Process [https://medium.com/@megtechcorner/write-your-own-os-4-boot-process-b7cb7bef2fcb]

Next: Write Your Own OS (6) — Control the cursor (Coming soon)

--

--