In previous episode, we learnt about switching to protected mode (32-bit), calling C code from assembly language, reading form hard disk, and accessing video memory to display message on the screen.
In this episode, we will create a basic video driver. Which will allow us to keep printing message on the screen by just calling a function, so that we do not have to worry about memory address every time. We are going to develop some of the following functionalities in our basic video driver:
This is very simple function, about printing on the screen. We are not stepping into graphics right now. We will step into graphics much later.
In earlier episode when we tried printing ‘X’ on the screen, it did but it printed on the screen at the beginning of the screen and not at the cursor position. Refer screenshot below:
First thing that we are going to do is get the cursor position, add the position into the video memory address that we have and print on the screen. To get a cursor position, we need to communicate with BIOS and get the position. We are going to write our code in C. To communicate with BIOS we need to write in low level language (Assembly), one of the beautiful thing about C language / compiler is that it allows us to write Assembly code in C language using __asm__ directive.
We will also do a bit of maintenance of a code so that we don’t have a single file containing all of the code. Let’s create a new file called ports.h in drivers folder.
unsigned char port_byte_in (unsigned short port);
void port_byte_out (unsigned short port, unsigned char data);
Below is the code for ports.c file in drivers folder:
#include "ports.h"
unsigned char port_byte_in (unsigned short port) {
unsigned char result;
__asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
return result;
}
void port_byte_out (unsigned short port, unsigned char data) {
__asm__("out %%al, %%dx" : : "a" (data), "d" (port));
}
Remember, now we are in protected mode. In protected mode if we want to communicate with the hardware, only way that we can communicate is via Ports. We will understand about ports more in detail as we go further. If you want to read more about Ports right now you can visit – http://www.brokenthorn.com/Resources/OSDev7.html
Our kernel.c file will look like following:
#include "ports.h"
void main() {
port_byte_out(0x3d4, 14); //read high byte of the cursor position
int xposition = port_byte_in(0x3d5); //store data returned in 0x3d5 register to xposition variable
xposition = xposition << 8; //convet into 8 bits
port_byte_out(0x3d4, 15); //read low byte of the cursor position
int yposition = port_byte_in(0x3d5); //store data returned in 0x3d5 register to yposition variable
int position = xposition + yposition; //add xposition and yposition bytes
position = position * 2; //data consist of 2 data - i.e. character, color of foreground and background
char* video_memory = (char*) 0xb8000;
video_memory[position] = 'X';
video_memory[position + 1] = 0x0f; //black background on white foreground - 0 = black; f = white
}
If you are wondering how to figure out which address (0x3d5, 0x3d4) and index (14, 15) to use, refer the below table:
Register Name | Port | Index | Mode 3h (80×25 Text Mode) |
Mode Control | 0x3C0 | 0x10 | 0x0C |
Overscan Register | 0x3C0 | 0x11 | 0x00 |
Color Plane Enable | 0x3C0 | 0x12 | 0x0F |
Horizontal Panning | 0x3C0 | 0x13 | 0x08 |
Color Select | 0x3C0 | 0x14 | 0x00 |
Miscellaneous Output Register | 0x3C2 | N/A | 0x67 |
Clock Mode Register | 0x3C4 | 0x01 | 0x00 |
Character Select | 0x3C4 | 0x03 | 0x00 |
Memory Mode Register | 0x3C4 | 0x04 | 0x07 |
Mode Register | 0x3CE | 0x05 | 0x10 |
Miscellaneous Register | 0x3CE | 0x06 | 0x0E |
Horizontal Total | 0x3D4 | 0x00 | 0x5F |
Horizontal Display Enable End | 0x3D4 | 0x01 | 0x4F |
Horizontal Blank Start | 0x3D4 | 0x02 | 0x50 |
Horizontal Blank End | 0x3D4 | 0x03 | 0x82 |
Horizontal Retrace Start | 0x3D4 | 0x04 | 0x55 |
Horizontal Retrace End | 0x3D4 | 0x05 | 0x81 |
Vertical Total | 0x3D4 | 0x06 | 0xBF |
Overflow Register | 0x3D4 | 0x07 | 0x1F |
Preset row scan | 0x3D4 | 0x08 | 0x00 |
Maximum Scan Line | 0x3D4 | 0x09 | 0x4F |
Vertical Retrace Start | 0x3D4 | 0x10 | 0x9C |
Vertical Retrace End | 0x3D4 | 0x11 | 0x8E |
Vertical Display Enable End | 0x3D4 | 0x12 | 0x8F |
Logical Width | 0x3D4 | 0x13 | 0x28 |
Underline Location | 0x3D4 | 0x14 | 0x1F |
Vertical Blank Start | 0x3D4 | 0x15 | 0x96 |
Vertical Blank End | 0x3D4 | 0x16 | 0xB9 |
Mode Control | 0x3D4 | 0x17 | 0xA3 |
Now if we compile the code and execute, we will be able to see X on the screen and at a correct position. i.e. where cursor is blinking.
Now let’s go further and create a very basic utility for screen part, so that we can start working on more important stuffs.
Let’s create a basic screen driver. We will create screen.h file inside drivers folder.
#define VIDEO_ADDRESS 0xb8000
#define TOTAL_ROWS 25
#define TOTAL_COLS 80
#define STANDARD_MSG_COLOR 0x0f //black bg, white foreground
#define ERROR_MSG_COLOR 0xf4 //red bg, white background
#define REG_SCREEN_CTRL 0x3d4
#define REG_SCREEN_DATA 0x3d5
void clear();
void printf(char* str);
and below is code for screen.c file inside drivers folder.
#include "screen.h"
#include "ports.h"
int CURRENT_CURSOR_POSITION = 0;
void set_cursor_position(int offset);
int get_cursor_position();
void clear() {
char* video_memory = (char*) VIDEO_ADDRESS;
int row = 0;
for(row = 0; row < TOTAL_COLS * TOTAL_ROWS; row++)
{
video_memory[row * 2] = ' ';
video_memory[row * 2 + 1] = 0x0f;
}
CURRENT_CURSOR_POSITION = 0x00;
set_cursor_position(CURRENT_CURSOR_POSITION);
}
void printf(char *str) {
char* video_memory = (char*) VIDEO_ADDRESS;
int pos = 0;
int cursorPosition = get_cursor_position();
while(str[pos] != 0)
{
video_memory[cursorPosition] = str[pos++];
video_memory[cursorPosition + 1] = 0x0f;
cursorPosition = cursorPosition + 2;
}
set_cursor_position(cursorPosition);
}
int get_cursor_position() {
port_byte_out(REG_SCREEN_CTRL, 14);
int position = port_byte_in(REG_SCREEN_DATA) << 8;
port_byte_out(REG_SCREEN_CTRL, 15);
position += port_byte_in(REG_SCREEN_DATA);
return position * 2;
}
void set_cursor_position(int offset)
{
offset /= 2;
port_byte_out(REG_SCREEN_CTRL, 14);
port_byte_out(REG_SCREEN_DATA, offset >> 8);
port_byte_out(REG_SCREEN_CTRL, 15);
port_byte_out(REG_SCREEN_DATA, offset & 0xff);
}
And we will make our kernel.c file sweet and simple:
#include "drivers/screen.h"
void main() {
clear();
char str[] = "Welcome to Learn OS. ";
printf(str);
char str1[] = "This message has been printed using printf.";
printf(str1);
}
With this, we are making our kernel bigger.. if you compile the code, you will notice that once we convert our kernel object file to elf32 file, kernel size increases up to 128MB. In boot.asm we are reading sectors from hard disk, so far we were reading only 1 sector from hard disk. Now 1 sector is not enough as our kernel size has increased. Even though size has increased by a huge number, actual kernel still occupies less space, but more then 1 sector size. So we are going to read 2 sectors now. In boot.asm file, look for load_kernel section, in that section we are reading sectors (line number 54). Let’s change that to 2. Code will look like following:
load_kernel:
mov bx, MSG_LOAD_KERNEL
call print
call print_nl
mov bx, KERNEL_OFFSET ;read from disk and store in 0x1000
mov dh, 2 ;read 2 sectors from HDD or bootable disk
mov dl, [BOOT_DRIVE]
call disk_load
ret
also let’s modify our compile.bat file:
echo off
echo "clean all binaries"
del *.bin
del *.o
del *.elf
echo "compile boot.asm"
fasm boot.asm
echo "compile loader.asm"
fasm loader.asm
echo "compile kernel.c"
wsl gcc -m32 -ffreestanding drivers/ports.h drivers/screen.h kernel.c drivers/ports.c drivers/screen.c -o kernel.o
echo "Producing elf file"
wsl objcopy kernel.o -O elf32-i386 kernel.elf
echo "Linking files"
wsl /usr/local/i386elfgcc/bin/i386-elf-ld -o kernel.bin -Ttext 0x1000 loader.o kernel.elf --oformat binary
echo "Creating image...."
type boot.bin kernel.bin > os_image.bin
echo "Launching QEMU"
qemu-system-x86_64 os_image.bin
Now, if we execute compile.bat file. It will execute the code and we will see output on the screen as below:
CONGRATULATIONS!!!! We have written our first driver.
Source code available at GitHub: https://github.com/dhavalhirdhav/LearnOS
That’s it for this post, next we will write a PCI IDE Controller driver to perform read and write operations on hard disk. We will also write our first File System into hard disk or look into a way to call Interrupts from Protected mode.