OS Coding 2 - BIOS based print function in 16-bit REAL mode
Writing a simple print function
OS Coding
This is a second entry in the series of OS coding. If you want to see more you can see all the entries for OS coding or just see the previous entry.
Purpose of it all
The main purpose of this series of entries is to document my journey through coding a very simple operating system from scratch. Some educational value might be there… somewhere… if you look deep enough.
I take no responsibility for any mental issues that might arise from reading this post!
General references for later use
- The current code on Github; might have more than shown here; the github code shows the last possible iteration of this “OS coding” blog entries
- OSDev.org - an excelent source for OS coding freaks
- Writing a Simple Operating System — from Scratch by Nick Blundell - a really nice, down to earth starting point that explains a lot of the things needed for starting. This entry is heavily influenced by this
This document references
Requirements
All the same requirements as in the first entry on the OS coding are necessary.
The task
In the previous entry we’ve seen that we can make BIOS load our boot sector but nothing really happened. All we did is stop the BIOS from shouting that we don’t really have a boot sector. We’ve added one with a very simple app that basically loops forever.
It would be a good idea to print something so that we know it’s actually working. A print function in assembly would be a good starting point. We can write a simple function in C but… let’s write one in assembly. It’s been ages and my code will be… fairly bad. But… let’s try to print out the word “Hello, World!” in assembly. We’ll be using a null-terminated string - a string that is terminated with a ‘0’ value (a common thing in computing) so that our print function knows where to start.
The general idea is to write a looping function that checks the current character, compares it to a zero, and then either proceeds to the next character or ends the execution. Some knowledge of pointers is necessary.
Print Function In C
Since we’re a bit rusty in ASM let’s see how that would look like in C, before we move to more obscure ways of doing this simple thing. So if we were to write it in C we would have something similar to:
void print_function(char *c)
{
while (*c != '\0')
{
// print out the character located at (*c)
c++;
}
}
This function:
- accepts a parameter
- gets the contents (value) of the memory currently pointed out by c
- compares the value (value) to zero
- if it’s not zero it will try to print out the character and increment the pointer by one byte so we can check the next character
- if it’s zero it ends the loop
- it’s important that the string is null-terminated otherwise we might start checking memory that is not ours
That function, in C, can be executed in a simple manner:
char *string = "Hello, World!\0";
print_function(string);
This code:
- declares the string (as a null-terminated string)
- executes the function by providing a parameter that is used to iterate through it’s characters
That code in C is noting special and it fairly simple. But we’re not writing in C yet. We need that code in assembly.
Print Function In Assembly
To print out the character we’ll be using the BIOS functions for now. The easiest way (probably) is to use the BIOS function teletype. Teletype prints out one character and moves the screen cursor forward.
To do that the teletype function uses a value in a specific register and is controlled by an interrupt. There’s a great number of possible interrupts but we’ll be using the int=10/ah=0x0e one.
To use the teletype print character function we need to:
- store a value (character) in the AL register
- store the value of 0x0e in AH (say that we’ll be using the BIOS teletype interrupt)
- execute the 0x10 interrupt function
In assembly that would be something like:
; assume we already have the character in AL
mov ah, 0x0e ; int=10/ah=0x0e -> BIOS teletype
int 0x10 ; AL already contains the character; print it out
This code assumes we already have a character in that specific register (AL). In the case of the C-language code we just provided a pointer. What happens on the assembly language in C is that, when we provide a parameter, that parameter goes on the stack or some specific register (depending on the C implementation). So, before we execute our assembly function, we need to have a common understanding where that string/parameter will be hend.
In the code before I’ve used SI. The assumptions is that the SI contains the start of a null-terminated string.
; use SI as string parameter
print_string:
pusha
mov bx, 0007h ; BH is DisplayPage, BL is GraphicsColor
start:
mov al, [si] ; <<<<<<<
cmp al, 0 ; if 0 then end
je done
mov ah, 0x0e ; int=10/ah=0x0e -> BIOS teletype
int 0x10 ; AL already contains the character
inc si ; >>>>>>>
jmp start
done:
popa
ret
The above code (from top to bottom):
- has different labels (print_string, start, done) so that we easily jump to different parts of the function
- pushes on the stack all general-purpose registers (AX, CX, DX, BX, original SP, BP, SI, and DI); if we modify them a simple popa will bring the original values, previously saved by pusha
- prepares the mode for the printing (0007h -> bx)
- copies the value pointed by SI to AL (move al, [si])
- compares the AL to 0
- je means “jump-if-equal”; if the value in AL is 0 - we jump to the end of the function and finish processing
- sets up AH so that it known we want teletype (int=10/ah=0x0e -> BIOS teletype)
- executes the 0x10 interrupt
- increases the SI register; it’s equivalent to the C’s “++”
- loops again by jumping to “start”
- upon ending it pops all registers by using popa; we’re cleaning after ourselves
We can save that code to “print_string.asm” and use that in our assembly boot-sector:
; Boot sector program that prints a starting sting
;
[org 0x7c00] ; Tell where this code starts in memory
mov si, HELLO_MSG ; Use SI as function parameter
call print_string
jmp $
%include "print_string.asm"
; Data
HELLO_MSG:
db `Hello, World!`, 0 ; null-terminated string
; Padding and magic number
times 510-($-$$) db 0
dw 0xaa55
There’s a couple of added things in relation to the first entry on the OS coding.
- we tell the assembler where to put our boot-sector program in memory; here it’s 0x7c00 in hope that it’s way past the initial BIOS memory location
- we declare the “Hello, World!” null-terminated string in the data block
- we include the previously-prepared print function
- we copy the location of the first character in the null-terminated string to SI (our funtion requires data to be in SI)
- we call the function pointed by the label print_string
The rest remains the same. The endless looping, the padding (to fit 512 bytes), the magic numbers at the end.
If our folder structure should looks something like (I’ve renamed loop.asm to boot_sect.asm):
source_folder:
|
| - bochs.conf
| - boot_sect.asm
| - print_string.asm
we can test it out.
Testing it all
Like previously, if our bochs is properly set up, we can build the boot sector with nasm:
nasm boot_sect.asm -f bin -o boot_sect_loop.bin
and run bochs:
bochs -f bochs.conf
If no errors we found (👀) bochs will show us our lovely hello string:
Final thoughts
So what was done here was fairly simple. We modified our boot sector, written before, so that a string can be printed out. Some assembly knowledge was required. We iterated through the memory-block each time checking if a particular character should be printed out or not. We’ve added a function to the assembly language and called it from our boot sector.