Introduction
Ok, You already know what kernel is https://en.wikipedia.org/wiki/Kernel_(operating_system)
The first part of writing an operating system is to write a bootloader in 16 bit assembly (real mode).
Bootloader is a piece of program that runs before any operating system is running.
it is used to boot other operating systems, usually each operating system has a set of bootloaders specific for it.
Go to following link to create your own bootloader in 16 bit assembly
https://createyourownos.blogspot.in/
Bootloaders generally select a specififc operating system and starts it's process and then operating system loads itself into memory.
If you are writing your own bootloader for loading a kernel you need to know the overall addressing/interrupts of memory as well as BIOS.
Mostly each operating system has specific bootloader for it.
There are lots of bootloaders available out there in online market.
But there are some proprietary bootloaders such as Windows Boot Manager for Windows operating systems or BootX for Apple's operating systems.
But there are lots of free and open source bootloaders.see the comparison,
https://en.wikipedia.org/wiki/Comparison_of_boot_loaders
Among most famous is GNU GRUB - GNU Grand Unified Bootloader package from the GNU project for Unix like systems.
https://en.wikipedia.org/wiki/GNU_GRUB
We will use GNU GRUB to load our kernel because it supports a multiboot of many operating systems.
Requirements
GNU/Linux :- Any distribution(Ubuntu/Debian/RedHat etc.).
Assembler :- GNU Assembler(gas) to assemble the assembly laguage file.
GCC :- GNU Compiler Collection, C compiler. Any version 4, 5, 6, 7, 8 etc.
Xorriso :- A package that creates, loads, manipulates ISO 9660 filesystem images.(man xorriso)
grub-mkrescue :- Make a GRUB rescue image, this package internally calls the xorriso functionality to build an iso image.
QEMU :- Quick EMUlator to boot our kernel in virtual machine without rebooting the main system.
Using the code
Alright, writing a kernel from scratch is to print something on screen.
So we have a VGA(Visual Graphics Array), a hardware system that controls the display.
https://en.wikipedia.org/wiki/Video_Graphics_Array
VGA has a fixed amount of memory and addresssing is 0xA0000 to 0xBFFFF.
0xA0000 for EGA/VGA graphics modes (64 KB)
0xB0000 for monochrome text mode (32 KB)
0xB8000 for color text mode and CGA-compatible graphics modes (32 KB)
First you need a multiboot bootloader file that instruct the GRUB to load it.
Following fields must be define.
Magic :- A fixed hexadecimal number identified by the bootloader as the header(starting point) of the kernel to be loaded.
flags :- If bit 0 in the flags word is set, then all boot modules loaded along with the operating system must be aligned on page (4KB) boundaries.
checksum :- which is used by special purpose by bootloader and its value must be the sum of magic no and flags.
We don't need other information,
but for more details https://www.gnu.org/software/grub/manual/multiboot/multiboot.pdf
Ok lets write a GAS assembly code for above information.
we dont need some fields as shown in above image.
boot.S
.set MAGIC, 0x1BADB002
.set FLAGS, 0
.set CHECKSUM, -(MAGIC + FLAGS)
.section .multiboot
.long MAGIC
.long FLAGS
.long CHECKSUM
stackBottom:
.skip 1024
stackTop:
.section .text
.global _start
.type _start, @function
_start:
mov $stackTop, %esp
call kernel_entry
cli
hltLoop:
hlt
jmp hltLoop
.size _start, . - _start
We have defined a stack of size 1024 bytes and managed by stackBottom and stackTop identifiers.
Then in _start, we are storing a current stack pointer, and calling the main function of a kernel(kernel_entry).
As you know, every process consists of different sections such as data, bss, rodata and text.
You can see the each sections by compiling the source code without assembling it.
e.g.: Run the following command
gcc -S kernel.c
and see the kernel.S file.
And this sections requires a memory to store them, this memory size is provided by the linker image file.
Each memory is aligned with the size of each block.
It mostly require to link all the object files together to form a final kernel image.
Linker image file provides how much size should be allocated to each of the sections.
The information is stored in the final kernel image.
If you open the final kernel image(.bin file) in hexeditor, you can see lots of 00 bytes.
the linker image file consists of an entry point,(in our case it is _start defined in file boot.S) and sections with size defined in the BLOCK keyword aligned from how much spaced.
linker.ld
ENTRY(_start)
SECTIONS
{
. = 1M;
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
}
Now you need a configuration file that instruct the grub to load menu with associated image file
grub.cfg
menuentry "MyOS" {
multiboot /boot/MyOS.bin
}
Now let's write a simple HelloWorld kernel code.
Simple :-
kernel_1 :-
kernel.h
#ifndef KERNEL_H
#define KERNEL_H
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200
uint16* vga_buffer;
#define NULL 0
enum vga_color {
BLACK,
BLUE,
GREEN,
CYAN,
RED,
MAGENTA,
BROWN,
GREY,
DARK_GREY,
BRIGHT_BLUE,
BRIGHT_GREEN,
BRIGHT_CYAN,
BRIGHT_RED,
BRIGHT_MAGENTA,
YELLOW,
WHITE,
};
#endif
Here we are using 16 bit vga buffer, on my machine the VGA address is starts at 0xB8000 and 32 bit starts at 0xA0000.
An unsigned 16 bit type terminal buffer pointer that points to VGA address.
It has 8*16 pixel font size.
see above image.
kernel.c
#include "kernel.h"
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{
uint16 ax = 0;
uint8 ah = 0, al = 0;
ah = back_color;
ah <<= 4;
ah |= fore_color;
ax = ah;
ax <<= 8;
al = ch;
ax |= al;
return ax;
}
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{
uint32 i;
for(i = 0; i < BUFSIZE; i++){
(*buffer)[i] = vga_entry(NULL, fore_color, back_color);
}
}
void init_vga(uint8 fore_color, uint8 back_color)
{
vga_buffer = (uint16*)VGA_ADDRESS; clear_vga_buffer(&vga_buffer, fore_color, back_color); }
void kernel_entry()
{
init_vga(WHITE, BLACK);
vga_buffer[0] = vga_entry('H', WHITE, BLACK);
vga_buffer[1] = vga_entry('e', WHITE, BLACK);
vga_buffer[2] = vga_entry('l', WHITE, BLACK);
vga_buffer[3] = vga_entry('l', WHITE, BLACK);
vga_buffer[4] = vga_entry('o', WHITE, BLACK);
vga_buffer[5] = vga_entry(' ', WHITE, BLACK);
vga_buffer[6] = vga_entry('W', WHITE, BLACK);
vga_buffer[7] = vga_entry('o', WHITE, BLACK);
vga_buffer[8] = vga_entry('r', WHITE, BLACK);
vga_buffer[9] = vga_entry('l', WHITE, BLACK);
vga_buffer[10] = vga_entry('d', WHITE, BLACK);
}
The value returned by vga_entry() function is the uint16 type with highlighting the character to print it with color.
The value is stored in the buffer to display the characters on a screen.
First lets point our pointer vga_buffer to VGA address 0xB8000.
Segment : 0xB800 & Offset : 0(our index variable(vga_index))
Now you have an array of VGA, you just need to assign specific value to each index of array according to what to print on a screen as we usually do in assigning the value to array.
See the above code that prints each character of HelloWorld on a screen.
Ok lets compile the source.
type sh run.sh command on terminal.
run.sh
as --32 boot.s -o boot.o
gcc -m32 -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
ld -m elf_i386 -T linker.ld kernel.o boot.o -o MyOS.bin -nostdlib
grub-file --is-x86-multiboot MyOS.bin
mkdir -p isodir/boot/grub
cp MyOS.bin isodir/boot/MyOS.bin
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o MyOS.iso isodir
qemu-system-x86_64 -cdrom MyOS.iso
Make sure you have installed all the packages that requires to build a kernel.
the output is
As you can see, it is a overhead to assign each and every value to VGA buffer, so we can write a function for that, which can print our string on a screen (means assigning each character value to VGA buffer from a string).
kernel_2 :-
kernel.h
#ifndef KERNEL_H
#define KERNEL_H
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200
uint16* vga_buffer;
#define NULL 0
enum vga_color {
BLACK,
BLUE,
GREEN,
CYAN,
RED,
MAGENTA,
BROWN,
GREY,
DARK_GREY,
BRIGHT_BLUE,
BRIGHT_GREEN,
BRIGHT_CYAN,
BRIGHT_RED,
BRIGHT_MAGENTA,
YELLOW,
WHITE,
};
#endif
digit_ascii_codes are hexadecimal values of characters 0 to 9. we need them when we want to print them on a screen.vga_index is the our VGA array index. vga_index is increased when value is assigned to that index.To print an 32 bit integer, first you need to convert it into a string and then print a string.
BUFSIZE is the limit of our VGA. To print a new line, you have to skip some bytes in VGA pointer(vga_buffer) according to the pixel font size.
For this we need another variable that stores the current line index(next_line_index).
#include "kernel.h"
uint32 vga_index;
static uint32 next_line_index = 1;
uint8 g_fore_color = WHITE, g_back_color = BLUE;
int digit_ascii_codes[10] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{
uint16 ax = 0;
uint8 ah = 0, al = 0;
ah = back_color;
ah <<= 4;
ah |= fore_color;
ax = ah;
ax <<= 8;
al = ch;
ax |= al;
return ax;
}
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{
uint32 i;
for(i = 0; i < BUFSIZE; i++){
(*buffer)[i] = vga_entry(NULL, fore_color, back_color);
}
next_line_index = 1;
vga_index = 0;
}
void init_vga(uint8 fore_color, uint8 back_color)
{
vga_buffer = (uint16*)VGA_ADDRESS;
clear_vga_buffer(&vga_buffer, fore_color, back_color);
g_fore_color = fore_color;
g_back_color = back_color;
}
void print_new_line()
{
if(next_line_index >= 55){
next_line_index = 0;
clear_vga_buffer(&vga_buffer, g_fore_color, g_back_color);
}
vga_index = 80*next_line_index;
next_line_index++;
}
void print_char(char ch)
{
vga_buffer[vga_index] = vga_entry(ch, g_fore_color, g_back_color);
vga_index++;
}
uint32 strlen(const char* str)
{
uint32 length = 0;
while(str[length])
length++;
return length;
}
uint32 digit_count(int num)
{
uint32 count = 0;
if(num == 0)
return 1;
while(num > 0){
count++;
num = num/10;
}
return count;
}
void itoa(int num, char *number)
{
int dgcount = digit_count(num);
int index = dgcount - 1;
char x;
if(num == 0 && dgcount == 1){
number[0] = '0';
number[1] = '\0';
}else{
while(num != 0){
x = num % 10;
number[index] = x + '0';
index--;
num = num / 10;
}
number[dgcount] = '\0';
}
}
void print_string(char *str)
{
uint32 index = 0;
while(str[index]){
print_char(str[index]);
index++;
}
}
void print_int(int num)
{
char str_num[digit_count(num)+1];
itoa(num, str_num);
print_string(str_num);
}
void kernel_entry()
{
init_vga(WHITE, BLACK);
print_string("Hello World!");
print_new_line();
print_int(123456789);
print_new_line();
print_string("Goodbye World!");
}
As you can see it is the overhead to call each and every function for displaying the values, that's why C programming provides a printf() function with format specifiers which print/set specific value to standard output device with each specifier with literals such as \n, \t, \r etc.
Keyboard :-
For keyboard I/O, use port number 0x60 with instructions in/out.Download kernel_source code fro keyboard. It reads keystrokes from user and displays them on screen.
#ifndef KEYBOARD_H
#define KEYBOARD_H
#define KEYBOARD_PORT 0x60
#define KEY_A 0x1E
#define KEY_B 0x30
#define KEY_C 0x2E
#define KEY_D 0x20
#define KEY_E 0x12
#define KEY_F 0x21
#define KEY_G 0x22
#define KEY_H 0x23
#define KEY_I 0x17
#define KEY_J 0x24
#define KEY_K 0x25
#define KEY_L 0x26
#define KEY_M 0x32
#define KEY_N 0x31
#define KEY_O 0x18
#define KEY_P 0x19
#define KEY_Q 0x10
#define KEY_R 0x13
#define KEY_S 0x1F
#define KEY_T 0x14
#define KEY_U 0x16
#define KEY_V 0x2F
#define KEY_W 0x11
#define KEY_X 0x2D
#define KEY_Y 0x15
#define KEY_Z 0x2C
#define KEY_1 0x02
#define KEY_2 0x03
#define KEY_3 0x04
#define KEY_4 0x05
#define KEY_5 0x06
#define KEY_6 0x07
#define KEY_7 0x08
#define KEY_8 0x09
#define KEY_9 0x0A
#define KEY_0 0x0B
#define KEY_MINUS 0x0C
#define KEY_EQUAL 0x0D
#define KEY_SQUARE_OPEN_BRACKET 0x1A
#define KEY_SQUARE_CLOSE_BRACKET 0x1B
#define KEY_SEMICOLON 0x27
#define KEY_BACKSLASH 0x2B
#define KEY_COMMA 0x33
#define KEY_DOT 0x34
#define KEY_FORESLHASH 0x35
#define KEY_F1 0x3B
#define KEY_F2 0x3C
#define KEY_F3 0x3D
#define KEY_F4 0x3E
#define KEY_F5 0x3F
#define KEY_F6 0x40
#define KEY_F7 0x41
#define KEY_F8 0x42
#define KEY_F9 0x43
#define KEY_F10 0x44
#define KEY_F11 0x85
#define KEY_F12 0x86
#define KEY_BACKSPACE 0x0E
#define KEY_DELETE 0x53
#define KEY_DOWN 0x50
#define KEY_END 0x4F
#define KEY_ENTER 0x1C
#define KEY_ESC 0x01
#define KEY_HOME 0x47
#define KEY_INSERT 0x52
#define KEY_KEYPAD_5 0x4C
#define KEY_KEYPAD_MUL 0x37
#define KEY_KEYPAD_Minus 0x4A
#define KEY_KEYPAD_PLUS 0x4E
#define KEY_KEYPAD_DIV 0x35
#define KEY_LEFT 0x4B
#define KEY_PAGE_DOWN 0x51
#define KEY_PAGE_UP 0x49
#define KEY_PRINT_SCREEN 0x37
#define KEY_RIGHT 0x4D
#define KEY_SPACE 0x39
#define KEY_TAB 0x0F
#define KEY_UP 0x48
#endif
inb() receives byte from specified port and returns it.
outb() send byte to specified port.
uint8 inb(uint16 port)
{
uint8 ret;
asm volatile("inb %1, %0" : "=a"(ret) : "d"(port));
return ret;
}
void outb(uint16 port, uint8 data)
{
asm volatile("outb %0, %1" : "=a"(data) : "d"(port));
}
char get_input_keycode()
{
char ch = 0;
while((ch = inb(KEYBOARD_PORT)) != 0){
if(ch > 0)
return ch;
}
return ch;
}
void wait_for_io(uint32 timer_count)
{
while(1){
asm volatile("nop");
timer_count--;
if(timer_count <= 0)
break;
}
}
void sleep(uint32 timer_count)
{
wait_for_io(timer_count);
}
void test_input()
{
char ch = 0;
char keycode = 0;
do{
keycode = get_input_keycode();
if(keycode == KEY_ENTER){
print_new_line();
}else{
ch = get_ascii_char(keycode);
print_char(ch);
}
sleep(0x02FFFFFF);
}while(ch > 0);
}
void kernel_entry()
{
init_vga(WHITE, BLUE);
print_string("Type here, one key per second, ENTER to go to next line");
print_new_line();
test_input();
}
Each keycode is being converted into its ASCII character by function get_ascii_char().
Box-drawing GUI :-
Download the kernel_source for drawing boxes used in old systems such as DOSBox etc.(kernel_source/GUI/)
Tic-Tac-Toe Game :-
We have printing code, keyboard I/O handling and GUI using box drawing characters.So lets write a simple Tic-Tac-Toe game in kernel that can be run on any PC.
Download the kernel_source code, kernel_source/Tic-Tac-Toe.
How to Play :
Use arrow keys(UP, DOWN, LEFT, RIGHT) to move white box between cells and press SPACEBAR to select that cell.
RED color for player 1 Box and BLUE color for player 2 box.
See Turn for which player has a turn to select cell.(Turn: Player 1)
If you are running this on actual hardware then increase the value of sleep() function in launch_game() in tic_tac_toe.c and in kernel_entry() in kernel.c So that will work normally and not too fast. I used 0x2FFFFFFF.
For more about os from scratch, os calculator and low level graphics in operating system, see this link.
References