Introduction

After making it past the bootloader, the actual operating system starts and displays an X in the upper left-hand corner of the screen using the following code:

void main () {
    char * video_memory = ( char *) 0xb8000 ;
    *video_memory = 'X';
}

That's not very exciting. And to make things even more complicated, most of the standard C library functions (like printf) aren't available to us. Therefore, the first order of business will be to implement a standard C library for KoizOS.

Printing to the screen

We could continue to print to the screen by writing directly to the memory, but it would be so much nicer just to build some helper functions to do it for us. Luckily, printing to screen in protected mode is pretty well documented and can be found on this OSDev link.

The following function builds off the concepts in that article and prints a character to the screen. It also handles new lines and will automatically scroll the display up if the text overflows past the visible display.

/** 
 * print_char() - internal helper method for printing a character
 *
 * @character:  - character to print
 * @col:        - which column to print character
 * @row:        - which row to print character
 * @char_color  - Leave char_color at 0 to do default black on white
 * 
 * Returns the new offset which is also the new position of the cursor
 */
int 
print_char(uint8_t character, uint8_t col, uint8_t row, uint8_t char_color) {

    uint8_t *vidmem = (uint8_t*) VIDEO_ADDRESS;
    
    /* Silently cancel for out of range values. TODO: Change this later */
    if(col >= MAX_COLS || row >= MAX_ROWS)
        return 0;

    if(!char_color)
        char_color = vga_make_display_color(
                        VGA_DISPLAY_COLOR_WHITE, 
                        VGA_DISPLAY_COLOR_BLACK
                    );

    uint16_t vidmem_offset = get_offset(col, row);

    /* Move to next line */
    if(character == '\n') {
        row = row + 1;
        vidmem_offset = get_offset(0, row);
    } else {
        vidmem[vidmem_offset] = character;
        vidmem[vidmem_offset+1] = char_color;
        vidmem_offset += VCHAR_CELLSIZE; 
    }
    
    /* Handle out of bounds by scrolling */
    if(vidmem_offset >= MAX_ROWS * MAX_COLS * VCHAR_CELLSIZE) {
        int i;
        int j;

        /* Copy next line to the current line in memory */
        for(i = 1; i < MAX_ROWS; i++)
            for(j = 0; j < MAX_COLS * VCHAR_CELLSIZE; j++)
                *(vidmem + get_offset(0, i-1) + j)  
                    = *(vidmem + get_offset(0, i) + j);

        /* Clear out the last line and decrement the offset */
        for(j = 0; j < MAX_COLS * VCHAR_CELLSIZE; j++)
            *(vidmem + get_offset(0, MAX_ROWS-1) + j) = 0;
        
        /* Fix offsets and variables */
        row = row - 1;
        vidmem_offset -= MAX_COLS * VCHAR_CELLSIZE;
    }

    /* Set new cursor position */
    set_cursor_offset(vidmem_offset);
    return vidmem_offset;

}

Replicating printf

My implementation of printf is not nearly as robust and powerful as the standard C implementation, but it handles things like printing integers, characters, and character strings just fine.


/*
 * A light-weight printf function
 * Supports printing integers, hexidecimal, and other strings.
 */
void printf(char *format, ...)
{
    va_list arg;

    va_start(arg, format);
    /*printf_impl(format, arg);*/
    while(*format != '\0') {

        /* Handle special characters */
        if(*format == '%') {
            if(*(format + 1) == '%') {
                std_print_char('%');
            } else if(*(format + 1) == 's') {
                char* subString = va_arg(arg, char*);
                std_print(subString);
            } else if(*(format + 1) == 'c') {
                char character_arg = va_arg(arg, int);
                std_print_char(character_arg);
            } else if(*(format + 1) == 'd') {
                unsigned int num = va_arg(arg, uint32_t);
                print_uint_to_string(num, 10);
            } else if(*(format + 1) == 'x') {
                unsigned int num = va_arg(arg, uint32_t);
                print_uint_to_string(num, 16);
            } else if(*(format + 1) == 'b') {
                unsigned int num = va_arg(arg, uint32_t);
                print_uint_to_string(num, 2);
            } else if(*(format + 1) == '\0') {
                std_error("printf error: next character is null");
                break;
            } else {
                std_error("printf error: Unknown escape sequence %");
                std_print_char(*(format + 1));
                break;
            }
            format++;
        /* Simply print the next character */
        } else {
            std_print_char(*format);
        }

        // Move to the next character
        format++;
    }
    va_end(arg);
}

The helper function that pops up for printing hexidecimal, decimal, and binary is also a lot easier than it looks. Interestingly, I built this function for another project forever ago so it was pretty simple to port it over to KoizOS. It takes advantage of the fact that the base 10 and base 2 number system can be defined as a subset of base 16.

/*
 * Helper function for the printf function
 */
static void print_uint_to_string(unsigned int number, unsigned int base)
{
    /* error checking */
    if(base > 16) {
        std_error("printf base cannot be greater than 16!");
        return;
    }

    /* handle printing different bases */
    if(base == 2)
        std_print("0b");
    if(base == 16)
        std_print("0x");

    /* if the number is 0, just print 0. save some processing */
    if(number == 0) {
        std_print_char('0');
        return;
    }

    /* 
     * otherwise convert the number into a string.
     * it'll be in reverse order so print it in reverse.
     */
    char buffer[PRINTF_BUFFER_SIZE];
    int current_pos = 0;
    const char numtochar[] = "0123456789ABCDEF";
    do {
        buffer[current_pos] = numtochar[number % base];
        number /= base;
        ++current_pos;
    } while(number != 0 && current_pos < PRINTF_BUFFER_SIZE);

    int i;
    for(i = current_pos; i > 0; i--)
        std_print_char(buffer[i-1]);
}

Panic Function

Having the operating system fail immediately on an error is important to prevent undefined behavior. The panic function does just that and halts the processor by putting it in an infinite loop. It prints directly to screen to prevent issues with buffered input once printf is set to print to standard streams.

__attribute__((__noreturn__))
void panic(char *message)
{
    vga_print_screen("\n", DEFAULT_ERROR_COLOR);
    vga_print_screen("KERNEL PANIC: ", DEFAULT_ERROR_COLOR);
    vga_print_screen(message, DEFAULT_ERROR_COLOR);
    while (1) { }
	__builtin_unreachable();
}

Tying it all together

There are a couple of other functions such as memset, and memcmp that I've implemented, but for the sake of not turning this post into any more of a code dump, I'll refrain from posting them here. With the implementation of a standard lib, our entire main.c file now looks like this.

#include "libc/stdlib.h"

void main () 
{
    kernel_init();

    /* Test out our screen print functions */
    int i;
    int j;
    for(i = 0; i < 202; i++) {
        print("abcdefghijklmnopqrstuvwxyz0123456789");
        j += 19;
    }
    printf("\n\nbooted up\n");
    printf("we know that %s is %d is %x is %b\n", "100", 100, 100, 100);

    /* Done */
    while(1) {
        kernel_update();

    }
	__builtin_unreachable();

}
    

Compiling and running in QEMU now fills our screen full of a nice wall of text that neatly scrolls!

What's next?

Now that a basic standard lib exists, the next order of business is to program interrupts which will allow us to do things like grab user input from the keyboard.