One of my goals for a long time was to write a simple standalone kernel in C, just to see if I could. Well, I finally figured it out, and this is how I did it.
I couldn’t have done this without the tutorials at osdev.org.
I have all the code here hosted at git://github.com/alecrn/kernel.git.
The tools
- Developed on Ubuntu Linux.
- Bochs for testing the kernel.
- NASM version 2.09 to compile assembly files.
- GCC version 4.5.2 to compile C files.
- ld version 2.21 to link the object files.
- GRUB version 0.97 for boot-loading.
- genisoimage to generate the bootable ISO file.
Directory setup
These are the directories in my project directory.
- bin – Compiled binary files.
- obj – Intermediate object files.
- scripts – Scripts and configuration for compiling and testing.
- src – My actual source files.
Loader file
The first file to consider is “loader.s” This is the assembly code that our bootloader, GRUB, will call to start the kernel going. It’s basically the glue between the bootloader and our more manageable C code.
global loader
extern kmain
MODULEALIGN equ 1 << 0
MEMINFO equ 1 << 1
FLAGS equ MODULEALIGN | MEMINFO
MAGIC equ 0x1badb002
CHECKSUM equ -(MAGIC + FLAGS)
section .text
align 4
MultiBootHeader:
dd MAGIC
dd FLAGS
dd CHECKSUM
STACKSIZE equ 0x4000
loader:
mov esp, stack + STACKSIZE
push eax
push ebx
call kmain
cli
hang:
hlt
jmp hang
section .bss
align 4
stack:
resb STACKSIZE
This file initializes the stack the kernel will use, and calls “kmain,” which is a function defined in a C file.
The kmain file
#include "clear_screen.h"
#include "write_str.h"
void kmain (void *mbd, unsigned int magic)
{
if (magic != 0x2badb002)
{
// This branch executes if the "magic number" was
// incorrect, meaning the bootloader failed but kmain was
// still called.
return;
}
else
{
char *bootLoaderName = (char*)((long*)mbd)[16];
clear_screen();
write_str(bootLoaderName);
write_str("\nHello, master.");
}
}
This is like the kernel equivalent of the “main” function. Notice the magic number check; the magic number is a particular constant that GRUB will pass to our program. If the magic number is not what it should be, then something must’ve gone wrong, and we exit.
Otherwise, we can read the bootloader’s name and write some text to the screen. The clear_screen call, which we will define later, is needed, otherwise all of the text that GRUB displays will be left up.
Next, we call write_str (which we will also define later) to print some text to the screen.
Textual output
To write to the screen, we have to write directly into video memory. The kernel is, by default, in text mode, which means the video memory is split up into a grid of characters, and you can simply write a character to the right byte and get the corresponding text on the screen.
First of all, I made a file containing some configuration for the video system in video_system.h and video_system.c:
// FILE video_system.h
#ifndef VIDEO_SYSTEM
#define VIDEO_SYSTEM
struct video_system_t
{
volatile char *pointer;
int width;
int height;
} video_system;
#endif
// FILE video_system.c
#include "video_system.h"
// Text mode is always 80 columns by 25 rows
struct video_system_t video_system = {(volatile char*)0xb8000, 80, 25};
video_system will contain “pointer,” the pointer to video memory; “width,” the width of the screen in chars; and “height,” the height of the screen in chars.
Video memory is always at 0xb8000, and the width and height are always (as far as I’ve seen) 80 and 25, respectively, in text mode.
Now we can define clear_screen. To understand how this works, you need to know how video memory is laid out.
Basically, assuming our screen is five by five characters, we could represent it like this:
(1 H) (1 e) (1 l) (1 l) (1 o) (1 I) (1 a) (1 m) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0) (0 0)
Here is the corresponding layout in video memory:
1H1e1l1l1o1I1a1m0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Each spot on the screen is represented by two bytes, of which the left one is the color and the right is the character.
Knowing just that, we can write clear_call:
// FILE clear_screen.h
#ifndef CLEAR_SCREEN
#define CLEAR_SCREEN
void clear_screen ();
#endif
// FILE clear_screen.c
#include "clear_screen.h"
#include "video_system.h"
void clear_screen ()
{
volatile char *video = video_system.pointer;
int i;
for (i = 0; i < video_system.width * video_system.height * 2; i += 2)
{
video[i] = ' ';
video[i + 1] = 0x00;
}
}
So this procedure simply runs through each of the (width × height × 2) bytes in video memory, setting the color to 0 (black) and the character to a space.
Now that we’ve cleared the screen, we need to be able to put characters and colors in the correct spots. Before we write the code that places a character into video memory, we should define a system for creating colors.
A color, as above, is a single byte, although it is split up into two colors: A foreground and background color, each of which has a three-bit color indicator (black, white, blue, etc.) and a one-bit brightness indicator (1 means bright, 0 means dim).
// FILE color.h
#ifndef COLOR
#define COLOR
typedef unsigned char color_t;
enum color_e
{
BLACK = 0,
BLUE,
GREEN,
CYAN,
RED,
MAGENTA,
BROWN,
WHITE
};
color_t make_color (int fore, int fore_bright, int back, int back_bright);
#endif
// FILE color.c
#include "color.h"
color_t make_color (int fore, int fore_bright, int back, int back_bright)
{
fore_bright &= 1; // Make sure they're only 1 or 0
back_bright &= 1;
return (back_bright << 7) | (back << 4) | (fore_bright << 3) | fore;
}
Now we can write a function, put_char, that places a character with a color at a given row and column on the screen:
// FILE put_char.h
#ifndef PUT_CHAR
#define PUT_CHAR
#include "color.h"
void put_char (char c, color_t color, int row, int col);
#endif
// FILE put_char.c
#include "put_char.h"
#include "video_system.h"
void put_char (char c, color_t color, int row, int col)
{
volatile char *video = video_system.pointer;
int index = 2 * ((row * video_system.width) + col);
video[index] = c;
video[index + 1] = color;
}
Another concern is writing characters one-by-one after the other, which is important in writing strings. To do that, the program must remember where it put the last character. I did this using a cursor structure:
// FILE write_cursor.h
#ifndef WRITE_CURSOR
#define WRITE_CURSOR
#include "color.h"
struct write_cursor_t
{
int row, col;
color_t color;
} write_cursor;
#endif
// FILE write_cursor.c
#include "write_cursor.h"
// Initialize cursor to position (0, 0) and white on black
struct write_cursor_t write_cursor = {0, 0, 0x0f};
Next, we’ll write a write_char functions that uses put_char and write_cursor together so that successive calls will write characters in order:
// FILE write_char.h
#ifndef WRITE_CHAR
#define WRITE_CHAR
void write_char (char c);
#endif
// FILE write_char.c
#include "write_char.h"
#include "put_char.h"
#include "write_cursor.h"
#include "video_system.h"
void write_char (char c)
{
// Break at a newline
if (c == '\n')
{
write_cursor.col = 0;
write_cursor.row += 1;
return;
}
put_char(c, write_cursor.color, write_cursor.row, write_cursor.col);
write_cursor.col += 1;
// Wrap at right edge of screen
if (write_cursor.col >= video_system.width)
{
write_cursor.col = 0;
write_cursor.row += 1;
}
}
Finally, it will now be trivial to write the write_str function:
// FILE write_str.h
#ifndef WRITE_STR
#define WRITE_STR
void write_str (const char *str);
#endif
// FILE write_str.c
#include "write_str.h"
#include "write_char.h"
void write_str (const char *str)
{
int i;
for (i = 0; str[i] != ''; ++i)
{
write_char(str[i]);
}
}
And that’s all as far as code! You now have enough to make a kernel with basic output abilities. I’ll leave keyboard input up to you (hint).
Compiling
Now, we have to compile the code.
First, we need to configure ld to properly link the object files so that GRUB can find our kernel. This is the file “scripts/linker.ld”:
ENTRY (loader)
SECTIONS {
. = 0x00100000;
.text : {
*(.text)
*(.rodata*)
*(.rdata*)
}
.rodata ALIGN (0x1000) : {
*(.rodata)
}
.data ALIGN (0x1000) : {
*(.data)
}
.bss : {
sbss = .;
*(COMMON)
*(.bss)
ebss = .;
}
}
We can also make a menu file, “scripts/menu.lst,” for GRUB to show when we boot up:
default 0 title MyOS kernel /boot/kernel.bin
This is what my compilation script, scripts/compile.sh, looks like:
# Display error message $1 and exit with code $2
function failure () {
echo $1
exit $2
}
# Return all sources suffixed by $1
function sources () {
echo ../src/*$1
}
# Directories we'll use
src=../src
obj=../obj
bin=../bin
# Clean the object and binary directories
rm $obj/* 2>/dev/null
rm $bin/* 2>/dev/null
# Assemble assembly files (output placed in .)
for file in `sources .s`
do nasm -f elf $file || failure 'Assembly failed' 1
done
echo 'Assembly successful.'
# Compile all C files to object files (output placed in same directory as C file)
gcc -c `sources .c` -Wall -Wextra -Werror -nostdlib -nostartfiles -nodefaultlibs || failure 'Compilation failed' 2
echo 'Compilation successful.'
# Move object files to their proper place
mv *.o `sources .o` ../obj
# Link the files
ld -T ../ld/linker.ld -o $bin/kernel.bin $obj/*.o || failure 'Link failed' 3
echo 'Linking successful.'
./make-image.sh
echo 'Done.'
The previous script calls another script, make_image.sh, which is responsible for taking the compiled kernel and placing it in an ISO CD image (note that it uses genisoimage and requires that you completed the included tutorial):
bin=../bin
isofiles=$bin/isofiles
grub=../grub
mkdir -p $isofiles/boot/grub
cp /usr/lib/grub/i386-pc/stage2_eltorito $isofiles/boot/grub
cp $grub/menu.lst $isofiles/boot/grub
cp $bin/kernel.bin $isofiles/boot
genisoimage -R -b boot/grub/stage2_eltorito -quiet -no-emul-boot \
-boot-load-size 4 -boot-info-table \
-o $bin/os.iso $isofiles
Here’s a configuration file for Bochs (place it in scripts/bochsrc.txt):
boot: cdrom ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14 ata0-master: type=cdrom, path="../bin/os.iso", status=inserted
And finally, a test script to load our kernel into Bochs and run it:
bochs -f bochsrc.txt
I hope that works for you, and that you have less pain than I did. If this code doesn’t work for you, I’d like to hear about it (I might even be able to fix it!).