*) Make a GCC cross compiler and install MonoDevelop and xorriso (for grub-mkrescue)
*) Create a new empty C project for your kernel.
*) Open the project options
*) Go to the Build->Configurations tab, delete "Release", and rename "Debug" to "i686-elf"
*) Go to the Build->Output, change the output name to "myos.bin", and replace "Debug" in the output path with "i686-elf"
*) Go to the Build->Code Generation->Code Generation tab, change the warning level to "all", change the optimization level to "2", and remove "DEBUG" from the define symbols.
*) Under "Extra Compiler Options", add the following:
Code: Select all
-std=gnu99 -ffreestanding -Wextra
Code: Select all
-T "/path/to/your/Project/Scripts/linker.ld" -ffreestanding -nostdlib
*) Go to the Build->Custom Commands tab and add a new "After Build" command with the command set to:
Code: Select all
bash "Scripts/mkdisk.sh"
*) Add a new "After Clean" command with the command set to:
Code: Select all
rm -rf "../myos.iso" "../ISO"
*) Go to the Run->Custom Commands tab and add a new "Execute" command with the command set to:
Code: Select all
qemu-system-i386 -cdrom "${ProjectDir}/../myos.iso"
*) Create a new directory called "Scripts".
*) Create a new file inside the "Scripts" directory called "mkdisk.sh" and add the following code to it:
Code: Select all
#!/bin/sh
mkdir -p ../ISO ../ISO/boot ../ISO/boot/grub
cp ./bin/i686-elf/myos.bin ../ISO/boot/myos.bin
cp ./Scripts/grub.cfg ../ISO/boot/grub/grub.cfg
grub-mkrescue -o ../myos.iso ../ISO
Code: Select all
menuentry "myos" {
multiboot /boot/myos.bin
}
Code: Select all
/* The bootloader will look at this image and start execution at the symbol
designated as the entry point. */
ENTRY(_start)
/* Tell where the various sections of the object files will be put in the final
kernel image. */
SECTIONS
{
/* Begin putting sections at 1 MiB, a conventional place for kernels to be
loaded at by the bootloader. */
. = 1M;
/* First put the multiboot header, as it is required to be put very early
early in the image or the bootloader won't recognize the file format.
Next we'll put the .text section. */
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
/* Read-only data. */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* Read-write data (initialized) */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* Read-write data (uninitialized) and stack */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
*(.bootstrap_stack)
}
/* The compiler may produce other sections, by default it will put them in
a segment with the same name. Simply add stuff here as needed. */
}
Code: Select all
# Declare constants used for creating a multiboot header.
.set ALIGN, 1<<0 # align loaded modules on page boundaries
.set MEMINFO, 1<<1 # provide memory map
.set FLAGS, ALIGN | MEMINFO # this is the Multiboot 'flag' field
.set MAGIC, 0x1BADB002 # 'magic number' lets bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS) # checksum of above, to prove we are multiboot
# Declare a header as in the Multiboot Standard. We put this into a special
# section so we can force the header to be in the start of the final program.
# You don't need to understand all these details as it is just magic values that
# is documented in the multiboot standard. The bootloader will search for this
# magic sequence and recognize us as a multiboot kernel.
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
# Currently the stack pointer register (esp) points at anything and using it may
# cause massive harm. Instead, we'll provide our own stack. We will allocate
# room for a small temporary stack by creating a symbol at the bottom of it,
# then allocating 16384 bytes for it, and finally creating a symbol at the top.
.section .bootstrap_stack, "aw", @nobits
stack_bottom:
.skip 16384 # 16 KiB
stack_top:
# The linker script specifies _start as the entry point to the kernel and the
# bootloader will jump to this position once the kernel has been loaded. It
# doesn't make sense to return from this function as the bootloader is gone.
.section .text
.global _start
.type _start, @function
_start:
# Welcome to kernel mode! We now have sufficient code for the bootloader to
# load and run our operating system. It doesn't do anything interesting yet.
# Perhaps we would like to call printf("Hello, World\n"). You should now
# realize one of the profound truths about kernel mode: There is nothing
# there unless you provide it yourself. There is no printf function. There
# is no <stdio.h> header. If you want a function, you will have to code it
# yourself. And that is one of the best things about kernel development:
# you get to make the entire system yourself. You have absolute and complete
# power over the machine, there are no security restrictions, no safe
# guards, no debugging mechanisms, there is nothing but what you build.
# By now, you are perhaps tired of assembly language. You realize some
# things simply cannot be done in C, such as making the multiboot header in
# the right section and setting up the stack. However, you would like to
# write the operating system in a higher level language, such as C or C++.
# To that end, the next task is preparing the processor for execution of
# such code. C doesn't expect much at this point and we only need to set up
# a stack. Note that the processor is not fully initialized yet and stuff
# such as floating point instructions are not available yet.
# To set up a stack, we simply set the esp register to point to the top of
# our stack (as it grows downwards).
movl $stack_top, %esp
# We are now ready to actually execute C code. We cannot embed that in an
# assembly file, so we'll create a kernel.c file in a moment. In that file,
# we'll create a C entry point called kernel_main and call it here.
call kernel_main
# In case the function returns, we'll want to put the computer into an
# infinite loop. To do that, we use the clear interrupt ('cli') instruction
# to disable interrupts, the halt instruction ('hlt') to stop the CPU until
# the next interrupt arrives, and jumping to the halt instruction if it ever
# continues execution, just to be safe. We will create a local label rather
# than real symbol and jump to there endlessly.
cli
hlt
.Lhang:
jmp .Lhang
# Set the size of the _start symbol to the current location '.' minus its start.
# This is useful when debugging or when you implement call tracing.
.size _start, . - _start
*) Create a new file under the main directory of the project called "kernel.c" and add the following content:
Code: Select all
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
enum vga_color
{
COLOR_BLACK = 0,
COLOR_BLUE = 1,
COLOR_GREEN = 2,
COLOR_CYAN = 3,
COLOR_RED = 4,
COLOR_MAGENTA = 5,
COLOR_BROWN = 6,
COLOR_LIGHT_GREY = 7,
COLOR_DARK_GREY = 8,
COLOR_LIGHT_BLUE = 9,
COLOR_LIGHT_GREEN = 10,
COLOR_LIGHT_CYAN = 11,
COLOR_LIGHT_RED = 12,
COLOR_LIGHT_MAGENTA = 13,
COLOR_LIGHT_BROWN = 14,
COLOR_WHITE = 15,
};
static const size_t VGA_WIDTH = 80;
static const size_t VGA_HEIGHT = 25;
size_t terminal_row;
size_t terminal_column;
uint8_t terminal_color;
uint16_t *terminal_buffer;
void terminal_initialize ();
void terminal_writestring (const char *data);
void
kernel_main ()
{
terminal_initialize ();
terminal_writestring ("Hello, kernel World!\n");
}
uint8_t
make_color (enum vga_color fg, enum vga_color bg)
{
return fg | bg << 4;
}
uint16_t
make_vgaentry (char c, uint8_t color)
{
uint16_t c16 = c;
uint16_t color16 = color;
return c16 | color16 << 8;
}
size_t
strlen (const char *str)
{
size_t ret = 0;
while (str[ret] != 0)
ret++;
return ret;
}
void
terminal_initialize ()
{
terminal_row = 0;
terminal_column = 0;
terminal_color = make_color(COLOR_LIGHT_GREY, COLOR_BLACK);
terminal_buffer = (uint16_t*) 0xB8000;
for (size_t y = 0; y < VGA_HEIGHT; y++)
{
for (size_t x = 0; x < VGA_WIDTH; x++)
{
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = make_vgaentry (' ', terminal_color);
}
}
}
void
terminal_setcolor (uint8_t color)
{
terminal_color = color;
}
void
terminal_putentryat (char c, uint8_t color, size_t x, size_t y)
{
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = make_vgaentry (c, color);
}
void
terminal_putchar (char c)
{
terminal_putentryat (c, terminal_color, terminal_column, terminal_row);
if (++terminal_column == VGA_WIDTH)
{
terminal_column = 0;
if (++terminal_row == VGA_HEIGHT)
terminal_row = 0;
}
}
void
terminal_writestring (const char *data)
{
size_t datalen = strlen(data);
for (size_t i = 0; i < datalen; i++)
terminal_putchar (data[i]);
}
*) Create a new script on your desktop called "MonoDevelop.sh" and add the following content:
Code: Select all
#!/bin/sh
if [ `whoami` = "root" ]; then
if [ "$1" = "setup" ]; then
export TARGET=i686-elf
export PREFIX=/path/to/your/crosscompiler
export PATH="$PREFIX/bin:$PATH"
if [ -f "/usr/bin/gcc.bak" ]; then
rm -f "/usr/bin/gcc"
else
mv -f /usr/bin/gcc /usr/bin/gcc.bak
fi
ln -s "$PREFIX/bin/$TARGET-gcc" /usr/bin/gcc
elif [ -f "/usr/bin/gcc.bak" ]; then
mv -f /usr/bin/gcc.bak /usr/bin/gcc
fi
else
echo "Sudo sandwich is icky, but MonoDevelop must run in user mode."
sudo -p "[sudo] password to setup /usr/bin/gcc: " $0 setup
monodevelop
sudo -p "[sudo] password to reset /usr/bin/gcc: " $0 reset
fi
*) Add water, shake, and bake at 375 degrees until golden brown. Serve hot out of the oven with an ice cold beverage.
------------------------------
And there you have it. The Bare Bones example works in MonoDevelop. You should be able to build, clean, and test your kernel in a really nice IDE now. In case anyone wants to know, the code for "kernel.c" is the same as the original Bare Bones code. It's just been refactored into GNU-style and reorganized a bit because I'm ocd.