Friday, April 13, 2007

Start Your Own OS Project in 30 Minutes

Have you ever dreamed of writing your own operating system? The first step is to get a kernel loaded. It's easier than you think. I'll show you how.

If you took an operating systems course in college, unless you went somewhere like Berkeley or Vrije in Amersterdam, it's likely that there was not a professor in your whole university who knew how to write an OS kernel from scratch and get it to boot by itself. Look, they're only human, ok? - most programmers wouldn't know how to do it. But it also means you came away from that class vaguely dissatisfied. Because if you take an operating systems class and never write your own simple kernel, it's like taking a driver's-ed course without ever getting to sit behind the wheel.

It's time we changed how we teach operating systems. Let's get started.

The first hurdle to overcome is getting the BIOS to boot your code. There are 512
bytes of code at the beginning of a disk, which the BIOS loads and executes. This is called the boot sector, and it is the responsibility of this incredibly tiny program to load other code and finally the actual kernel.

My first intention was to write one of these boot sector programs, because it would be a way to get code running on the machine completely by itself. There is just enough space in 512 bytes to write a very simple program. But that doesn't sound very useful, and besides, it's been done before.

So I wanted to contribute something new. Besides my idea of putting a toy operating system in the boot sector not being original, there is already a perfectly good boot loader program available. It's called GRUB. Why not put GRUB in your boot sector instead of custom code, and use GRUB to bootstrap your prototype OS? Suddenly what would have been weeks slogging through nasty bootstrap code isn't necessary. Things are looking up.

GRUB stands for GRand Unified Bootloader. If you dual-boot Linux and Windows, you may know GRUB can load your favorite operating systems, but did you know there's another reason it's called Unified? The people who developed GRUB looked at all these operating systems floating around; they all need many of the same things to happen as they boot, but each implements their own proprietary boot loader. They said, "What if there were a universal standard for booting an operating system? Then we could write one really good boot loader that would suffice for any operating system." They did.

What they came up with was the Multiboot Specification. It's a magic header that you include in a program to tell GRUB the information it needs to know to load your kernel. GRUB is smart enough to read a file system, so you don't even have to place your kernel in specific sectors or make some kind of wacky system file. Instead, all you have to do is compile a program as you would normally, and you can place the ordinary program file in any directory on the disk.

In an article I read recently, Julio Vidal describes how he made NetBSD Multiboot-Compliant. The article includes a detailed description of the boot process, and is an interesting read all around. Most importantly for us, though, the article links to some example code, from the GRUB documentation, which shows how to make a minimal kernel that is multiboot-compliant.

You can find that example code here:

Don't worry, you don't need all the files listed on that page - just three of them. You need boot.S, kernel.c, and multiboot.h. In order to illustrate the proof of concept (that, once loaded, I can run any code I want), I made a slight modification to the files which simply displays my own custom message after the kernel boots. You may want to work with the modified version in order to see where to put your own code, and because it is the version I used for these instructions, although the unmodified files should work too. You can get them from my downloads page: boot.S | kernel.c | multiboot.h

Now that you have the code, you need to compile it. For this, you will need to be running Linux. You'll also need Linux for the later steps involving GRUB, so you might as well get it now if you don't have it already. I recommend Ubuntu. Ubuntu comes on a Live CD, so you can run it from the CD without even having to install it. You can also install Ubutunu on an external hard drive or even a USB memory stick. (I will be posting another blog entry soon explaining how I did that.)

Compiling the code is the second major hurdle to cross. Most C programs link in libraries for things such as I/O and debugging. These C libraries in turn depend on an OS, or more specifically, they rely on making calls to the kernel of an OS. Well hold on a minute - we haven't got a kernel yet; that's what we're trying to make. Not only can we not rely on the the standard libraries, we have to make sure the compiler doesn't sneak external references in anyway (which it will try to do if you let it). So we have to use special compiler options to force the compiler to generate code that stands alone and has no external dependencies.

Open a terminal in Linux and type:
cd /whatever/directory/you/have/the/code/in

Then use this command (all on one line) to compile it:
gcc boot.S kernel.c -o mbs-kernel -ffreestanding -nostdlib -nostartfiles -fno-stack-protector


  • -o mbs-kernel: names the output file
  • -ffreestanding: means the resulting program should have no dependencies
  • -nostdlib: tells the linker not to bring in the C standard library, which won't work in the kernel
  • -nostartfiles: tells the linker not to bring in any C startup code
  • -fno-stack-protector: is a fix for the error "undefined reference to '__stack_chk_fail'"

This produces the file "mbs-kernel" which contains our new kernel.

You may need to be root to do the rest of the steps. To switch to superuser mode so that you can run all the commands, type:

sudo su

...and provide the root password when it asks.

Next we need to install grub to a floppy disk so that we can boot from it. It is possible to use a USB key instead, but I haven't figured out how to do that yet. So we'll use a floppy disk for this example.*

Open a terminal and type the following command:
grub --batch --device-map=/dev/null <<EOF

The above command starts GRUB in batch mode. Insert a formatted floppy disk and type these commands to install GRUB to it:

device (fd0) /dev/fd0
root (fd0)
setup (fd0)

If you are using a USB external floppy (what I was using), then the device will be different. In that case, do not type the above, but use this set of commands instead:

device (fd0) /dev/sdb
root (fd0)
setup (fd0)

USB floppy drive devices show up as /dev/sda, /dev/sdb, etc. Mine was sdb because I also have an internal SATA hard drive, and that occupies /dev/sda, so the floppy became /dev/sdb. If you have an ordinary IDE hard drive, then a USB floppy might be /dev/sda. How can you tell which one is your floppy disk? What I did was I used the Gnome file manager to mount the floppy disk (by double clicking its icon). Once I could see the contents of the floppy disk in the GUI file manager, I knew it had to be mounted. Then I opened up a terminal and typed:

mount -l

That's a lowercase "el". It caused mount to list all the mounted file systems - I looked at the one that matched where my floppy was mounted and the device given was the device I used for grub. However, if I recall, the drive has to actually be not mounted for the GRUB install to work. So after you mount the disk to find out its device file, be sure to right-click on it in the file manager and select "unmount" before you attempt to do the GRUB install.

Now that we have GRUB installed, we need to put our compiled kernel onto the disk. Use the file manager to mount the floppy drive and copy the file mbs-kernel to the /boot directory of the floppy disk.

*Note: If you already have GRUB on your hard drive, you don't necessarily have to have a boot floppy at all for this exercise. If you are comfortable with the idea of changing the boot configuration of your computer, you can safely skip the GRUB floppy install steps above, and place your homemade kernel directly in your /boot directory on your hard drive next to your Linux kernel. When you boot, you can choose whether to load Linux or your own kernel.

The last step is to modify the menu.lst file to make an entry for your kernel. If you have put your kernel on a floppy disk, you should be able to go to /boot/grub and double click on menu.lst in the file manager, which will open up gedit and let you edit the file. If you are going to run the kernel off your hard drive, you will find that gedit cannot save menu.lst if you opened it by double clicking. This is just a security precaution by Linux - the file manager didn't run gedit as root when you double clicked the file, so it can't write to it to save your changes. No matter - you can easily start gedit from the superuser prompt, and then it will have root priviledges. Just type:

gedit /boot/grub/menu.lst

Or, if you're back at the $ prompt instead of #, type:

sudo gedit /boot/grub/menu.lst

Menu.lst is simply a script that GRUB reads to know what boot menu options to show and how to execute them. At the very bottom of the file, if you are using a floppy, add the lines:

title Multiboot Kernel Example
root (fd0)
kernel /boot/mbs-kernel

If you want to boot your kernel directly from your hard drive, put the file mbs-kernel into your /boot directory on your hard drive, and add this to the bottom of your menu.lst file in /boot/grub/menu.lst:

title Multiboot Kernel Example
root (hd0)
kernel /boot/mbs-kernel

Once you have prepared the disk, reboot your computer. If you are booting from a floppy, make sure your BIOS is set to be able to boot from the floppy drive. Some computers have a key you can press to get a boot menu and choose manually. GRUB should start up when the computer boots. Choose the Multiboot Kernel Example, and GRUB will start your kernel. If you were successful, you should see either:
  1. A screen full of gobbledygook if you booted the default GNU example code
  2. A friendly message if you booted my version
If you get a message from GRUB that says "Error 28: Selected item cannot fit into memory," that error message is badly worded. It really means, "Default load address is too high." I got the error on a machine with 32 MB of RAM, but not on one with 256 MB. This problem could be fixed by instructing GCC to compile the program for a lower memory address. Anyone know how to do that?

In any case, as soon as you get the kernel to boot, you should immediately boot back into Linux, delete my code changes, and add your own code instead. This is your kernel.

If you want to go further, here are some resources:

Remember, you are starting with a blank slate. You won't have a filesystem driver, I/O, anything, until you put them into your OS. You are starting at the bare metal. One way of looking at it is that you have nothing holding up your code.

Another way of looking at it, is that you have nothing holding you back.

Happy hacking.
Post a Comment