0% found this document useful (0 votes)
130 views13 pages

Basic Kernel

This document discusses building and running a basic kernel called "HelloWorld" on a virtual machine using Qemu. It describes how to: 1) Create a Qemu boot image file containing Grub, a Grub menu listing the kernel, and the kernel itself. 2) Use tools like dd, mtools, and mcopy to format the image as a disk, create directories and partitions, and copy over the necessary Grub files. 3) Add a "device map" file to map Grub's disk naming conventions to the host system's conventions like Linux uses, allowing Grub to boot the kernel on the image.

Uploaded by

Daniel Orellana
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
130 views13 pages

Basic Kernel

This document discusses building and running a basic kernel called "HelloWorld" on a virtual machine using Qemu. It describes how to: 1) Create a Qemu boot image file containing Grub, a Grub menu listing the kernel, and the kernel itself. 2) Use tools like dd, mtools, and mcopy to format the image as a disk, create directories and partitions, and copy over the necessary Grub files. 3) Add a "device map" file to map Grub's disk naming conventions to the host system's conventions like Linux uses, allowing Grub to boot the kernel on the image.

Uploaded by

Daniel Orellana
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Chapter 1

A basic kernel
In this chapter, we will show how to build and run the most basic of kernels1 . In keeping with programming tradition, we will call the kernel HelloWorld, although, as the world in which our code operates gets destroyed almost as soon as it starts running, a more appropriate name might have been GoodbyeWorld, cruel or not. All source les corresponding to this chapter can be found atxs http://www.cs.vu.nl/~herbertb/ misc/writingkernels/src/basickernel_helloworld.tgz.

1.1

The bare basics: running on the (virtual) metal

Our aim is to build a real kernel that you can run on a normal x86 machine. Doing so has two implications. First, we will not have any support if we do not add it to our kernel ourselves. Specically, none of the helpful libraries and services are available to help us write, run, and debug our programs. Even printing on the screen, for instance, is not trivial. The C printf function does not exist yet, and neither do any of the library function. As an aside, while testing and coding, you probably do not want to reboot your machine every single time you have made a small change to your kernel. It is better if we do all our development on a virtual machine. To our kernel, these machines provide an illusion of hardware, but in reality they are simply programs that run on our normal operating system. In this text, we will use Qemu [1] as our virtual machine. Qemu is convenient and runs on Linux, BSD, Mac OS X, and even Windows. It is important to emphasise, however, that the virtual machine is for convenience only. All that we develop works equally well on real hardware. You may wonder how this works. After all, a physical machine typically boots from a hard drive partition2 . The answer is deceptively simple. We will make a boot image, a le that pretends to be a partition, and that contains the kernel, some code to load and execute that kernel, and every thing else we need. Qemu can use this image as if it is a real partition and boot from it. So, if we copy our kernel on a real partition, we boot from a real drive and if we copy it to our generated boot image for Qemu, we can boot it in Qemu. This brings us to the second implication: bootstrapping. We need a way to tell our machine to pick up our kernel from disk, load it in memory with all bits at the right addresses and execute it. While it is entirely possible to write such a bootloader from scratch, it is tedious and, certainly on x86, it is not needed. On the x86 platform we can avail ourselves of GNUs Grand Unied Bootloader (GRUB), familiar to most Linux users and even users of Solaris on x86. Grub implements the multiboot specication, an open standard originally created in 1995, that describes how compliant (multiboot) kernels can be loaded. What it means exactly, to be multiboot compliant, is not very
1 This 2 PCs

document is inspired by Brandon Friesens Kernel Development Tutorial [2]. can also boot from oppy disks, CD ROMS, and these days even from USB sticks

CHAPTER 1. A BASIC KERNEL

important at this stage. We will just have to make sure that we are, so we can load our kernel using Grub. In this rst implementation chapter, we will just try to get something up quickly, without thinking about the hows and whys too much. Later, we will look at certain bits of the process in more detail. In later chapters, we will add more functionality to our trivial HelloWorld kernel. The idea is that after a few iterations, we will arrive at a kernel that is useful in the sense that it supports processes, virtual memory, keyboard input and console output, as well as a rudimentary le system.

1.2

The kernel coders tools: what do we need?

In this text, we assume that we build our kernel on Linux, or cygwin3 on Windows, so that we can use a uniform and convenient set Unix tools. Cygwin needs to be installed and setup to include a set of development tools, as described below. For Linux, I am using distributions like Debian or Ubuntu, but there is no reason why you could not use some other distribution. The only real dierence is in the installation of software packages. On Debian/Ubuntu we use apt-get for this purpose. While Red Hat, Slackware, and other distributions may not support this particular command, they will have equivalent ways of installing software. The things that you really need for this course are: 1. PC: just your normal working machine will do 2. qemu: a whole system emulator (www.qemu.org/) 3. gcc: the GNU C compiler (gcc.gnu.org/) 4. make: a utility for automatically building executable programs from source code (www.gnu.org/software/make/) 5. gas: the GNU assembler and other binary utilities (www.gnu.org/software/binutils/) 6. grub: the Grand Unied Bootloader (www.gnu.org/software/grub/) 7. mtools: open source collection of tools to allow Unix Operating System to manipulate les on an MS-DOS lesystem (www.gnu.org/software/mtools) On Debian/Ubuntu you simply install software that is not yet available by means of apt-get. For instance, to install all of the above, programs you may type:
sudo apt g e t i n s t a l l qemu gcc 4.0 make b i n u t i l s grub m t o o l s

Depending on your current conguration, you may have some of these packages installed already. However, all of these programs are standard development tools that you can just install using whatever your local equivalent of apt-get may be (except for the PC, which you obtain with your local equivalent of euros). .

1.3

Preparing a Qemu boot image

At some point during this chapter, we will have a bootable kernel. Let us think about what to do with it when we do. As mentioned, we will use Grub as a bootloader to load and start our kernels. That means that we still need an image with grub installed on it. We also have to make sure that our kernel is on the image. In addition, we will have to add a menu for grub that lists all the kernels it can boot. Grub is quite powerful, you see, and it can boot a machine with any of a set of kernels listed in the Grub menu. The menu is typically called the menu.lst le. In our
3 www.cygwin.com

1.3. PREPARING A QEMU BOOT IMAGE

case, we need to add an entry that points to our kernel. Summarising, we need to create an image le and we need to store Grub, a grub menu, and a kernel on this image le. The way we create a bootable image is by means of the dd tool and the programs that come with mtools. The dd tool is a common Unix program whose primary purpose is the low-level copying and conversion of raw data. We use dd to create an zero-lled image called core.img with 088704 blocks of 512 bytes each.
dd i f =/dev / z e r o o f=c o r e . img c o u n t =088704 bs =512

Now, we have to transform this raw image into something that carries a le system layout. We use mtools for this purpose. First we make sure that all mtools commands work on our image core.img.
echo " d r i v e c : f i l e = \ " pwd / c o r e . img \ " p a r t i t i o n = 1 " > / . m t o o l s r c

The tools will automatically pick up the le .mtoolsrc from our home directory now. So we no longer have to specify that we want to execute the following commands on this image. First we create a c: partition. The -I option means that the parition table will be initialised and all existing partitions, if any, destroyed. In our case, we do not have partitions yet, but we do need a new partition table.
m p a r t i t i o n I c :

Next, we create (option -c) a new partition, with 88 cylinders (-t), 16 heads (-h), and 63 sectors per track. Note that 88 16 63 = 88704. In other words, it matches the number of sectors we made on our image.
m p a r t i t i o n c t 88 h 16 s 63 c :

Next, we have to format our newly created partition. We format it in MS DOS format (FAT16), as this is nice and simple, and good enough for our purpose:
mformat c :

Given this le system image, we can add directories and copy les. For instance, let us create the following two directories:
mmd c : / b o o t mmd c : / b o o t / grub

We are getting there, but we are not done yet. We also have to make Grub available on the image. It may well be that you are already using Grub on your real machine. If not, you need to install it somewhere on your hard drive. I will assume that Grub lives in /boot/grub/ on your real machine, which is where it will typically live if you are using Grub yourself. We need to copy a few les to our new image. The precise use of each of these Grub les is not very important. Suce to say that Grub needs them all. We will copy them into the directory c:/boot/grub
mcopy / b o o t / grub / s t a g e 1 c : / b o o t / grub mcopy / b o o t / grub / s t a g e 2 c : / b o o t / grub mcopy / b o o t / grub / f a t s t a g e 1 5 c : / b o o t / grub

Presently, we need a bit of Grub magic to provide a device map. It is not very important, but basically, the story is as follows. Grub has its own way of naming hard disks and partitions. Not surprisingly, its conventions dier from the Linux device names. Linux uses such names as /dev/hda1. The rst hard disk on Linux is called /dev/hd0, while the oppy drive is called /dev/fd0. The four primary partitions allowed per disk are numbered from 0 to 3. Logical partitions are counted beginning with 4. So we end up with something like: (hd0,0) (hd0,1) (hd0,2) (hd0,3) (hd0,4) (hd0,5) first primary partition on first hard disk second primary partition third primary partition fourth primary partition (usually an extended partition) first logical partition second logical partition ...

CHAPTER 1. A BASIC KERNEL

All hard disks detected by the BIOS or other disk controllers are simpply counted according to the boot sequence in the BIOS itself. As the BIOS device names do no match up with Linux device names, we need a mapping between the two. Grub stores this mapping in a le called the device map. In our case, we have called the mapping le bmap, and we add an entry in it to say that (hd0) is the core.img le. Plus we specify what sort of partition this is and that our boot partition hd0 is the rst partition on disk. Finally, with setup we write the boot sector on the rst disk:
echo " ( h d 0 ) c o r e . i m g " > bmap p r i n t f " g e o m e t r y ( h d 0 ) 8 8 1 6 6 3 \ n r o o t ( hd0 , 0 ) \ n s e t u p ( h d 0 ) \ n " | / u s r / s b i n / grub \ d e v i c e map=bmap b a t c h

All we need now is a menu and a working kernel. The latter is the topic of the remainder of this chapter, but the former is easy. Let us assume that when we nally do have a working kernel (HelloWorld or otherwise), we will call it kernel.bin and store it in c:/boot/grub/. In anticipation of this kernel, we can make a Grub menu that contains an entry for it. To do so, create a le called menu.lst with the following content:
s e r i a l u n i t =0 s t o p =1 s p e e d =115200 p a r i t y=no word=8 t e r m i n a l t i m e o u t=0 s e r i a l c o n s o l e default 0 timeout = 0 t i t l e = mykernel k e r n e l =/b o o t / grub / k e r n e l . b i n # module=/b o o t / g r u b / a d d i t i o n a l m o d u l e s

The rst two lines specify that we can control our computer using a serial line. We specify the speed of the connection (115200 bps), parity, and a few other things. The next few lines specify that kernel 0 is called mkernel and that it is the default kernel that will be immediately booted and that the kernel that goes with it is the one specied. We now copy this le to core.img
mcopy menu . l s t c : / b o o t / grub /

And we are done. That is, we have an image eagerly anticipating a kernel. As soon as we have one, we can copy it to the image too:
mcopy k e r n e l . b i n c : / b o o t / grub /

Assuming there are no bugs in the kernel it will be booted as soon as we turn on the machine. The equivalent to turning on the machine for Qemu is to start the virtual machine:
qemu hda c o r e . img

At this point, of course, this will not do anything yet. We rst need the kernel. The remainder of this chapter is devoted to creating a (trivial) kernel that can be booted either in the virtual machine, or on real hardware. It will be equally unimpressive in either case. However, the rst HelloWorld in kernel terms is a giant leap forward. Once we have any code running on our machine, we can also add any code we want.

1.4

A World kernel (without Hellos)

One thing that is easy to minimise but hard to avoid entirely in writing kernels is some assembly language. The very rst instructions that are executed are written in assembly (the entry point) and so is some of the code that deals with interrupts, the code that turns on paging, etc. Assembly language, even with an instruction set as ugly as that of the x86, should not frighten us. Most of it is straightforward and where it is not, we will furnish ample explanation. We want to use Grub. This implies, as mentioned earlier, that we should make our kernel multiboot compliant. In the old days, every OS came with its own set of boot mechanims. If the boot mechanism that came with your OS was not exactly what you wanted, tough. Making multiple OSs (such as your HelloWorld kernel and Windows or Linux) coexist together peacefully, was a real challenge. The multiboot specication addresses this problem. It species an interface

1.4. A WORLD KERNEL (WITHOUT HELLOS)

between the boot loader and the OS, so that any bootloader that is multiboot compliant should be able to boot any OS that is also multiboot compliant. Grub certainly is multiboot compliant. How do we make our kernel compliant too? A minimum requirement to be multiboot compliant is to have a header with the following three 32 bit words somewhere in the rst 8 KB of your le: 1. The multboot magic number: 0xbadboo2 2. The multiboot header ags. The ags specify features that the OS requests or requires of the bootloader (e.g., information about available memory that should be provided by grub to our kernel). For now, we will set the ags to 0x03. We will look at the multiboot header ags in a bit more detail later. 3. A checksum, which can simply be set to multibootHeaderFlags - multibootMagicNumber Finally, it is time to have a look at some code. We will start with the most trivial kernel written in C that we can imagine. Without printing anything, it will just add up two numbers and die: Listing 1.1: main.c
void cmain ( ) { i n t sum = 1+1; // even i n t h i s w o r l d 1 and 1 makes 2 }

As this trivial C program does not print anything to screen, we will not be able even to check whether it runs, and if so, whether it runs correctly. Let us not worry about that now. We will add basic I/O functions soon enough. First, we have to make sure that the C code gets executed. Grub will not do this for us, but we can make it execute some bootstrap code in assembly that subsequently calls into our C code. Listing 1.2 shows very basic bootstrap code that allows us to call into our kernel. At rst sight, it looks like a sizeable program. However, the le contains hardly any assembly. Most of it is lled with denitions (and comments!). In a nutshell, the following takes place: 1. Grub starts executing our kernel by means of a jump to the (assembly) entry point for our kernel, which is the instruction at the address indicated by the global label start (line 26). Now we are out of Grubs clutches and executing our own code. 2. We make sure that the magic values for multiboot are stored almost immediately after the entry point (lines 3740). 3. Since these are values and not executable code, we jump over them (the jump in line 32 will continue execution at line 42). 4. We want to leave assembly as quickly as possible. All we want is to set up a stack and then start executing C code. By convention, stacks grow from high to low, so in line 47 we load the stack pointer (esp) with the address of the top of the stack (space for the stack is dened in line 64). 5. All we need to do now is call our C function (line 50); assume that we will call the function to call cmain. 6. When we return from our C code we enter an innite loop in line 53. The hlt instruction is meant to halt the CPU when no immediate work needs to be done. It is run in Windows in the System Idle Process.

CHAPTER 1. A BASIC KERNEL Listing 1.2: boot.S: bootstrap code with kernel entry point

1. 2. 3. 4. 5. 6. 7. 8. 9. 10 . 11 . 12 . 13 . 14 . 15 . 16 . 17 . 18 . 19 . 20 . 21 . 22 . 23 . 24 . 25 . 26 . 27 . 28 . 29 . 30 . 31 . 32 . 33 . 34 . 35 . 36 . 37 . 38 . 39 . 40 . 41 . 42 . 43 . 44 . 45 . 46 . 47 . 48 . 49 . / 50 . 51 . 52 . 53 . 54 . 55 . 56 . 57 . 58 . 59 . 60 . 61 . 62 . 63 . 64 .

/ b o o t . S b o o t s t r a p t h e k e r n e l / / f i r s t we g i v e some d e f i n i t i o n s , t o make our code more r e a d a b l e l a t e r / #d e f i n e ASM 1 0x1BADB002 / magic n o . f o r m u l t i b o o t hea der / 0 x00000003 / f l a g s f o r m u l t i b o o t he ader / / s i z e o f our s t a c k (16KB) /

#d e f i n e MULTIBOOT HEADER MAGIC #d e f i n e MULTIBOOT HEADER FLAGS #d e f i n e STACK SIZE 0 x4000

/ On some s y s t e m s we have t o jump from a s s e m b l y t o C by r e f e r r i n g t o t h e C name p r e f i x e d by an u n d e r s c o r e . This i s a l l d e f i n e d d u r i n g c o n f i g u r a t i o n . We do not worry a b o u t i t h e r e and make s u r e we can h a n d l e e i t h e r (HAVE ASM USCORE i s d e f i n e d by c o n f i g u r e ) . / #i f d e f HAVE ASM USCORE # d e f i n e EXT C( sym ) #e l s e # d e f i n e EXT C( sym ) #e n d i f ## sym sym

/ The t e x t segment w i l l c o n t a i n code , t h e d a t a segment d a t a and t h e b s s segment z e ro f i l l e d s t a t i c v a r i a b l e s / .text .globl start : / As we need t h e m u l t i b o o t magic v a l u e s i n t h e b e g i n n i n g o f our f i l e , we w i l l add them p r e s e n t l y . The r e a l code c o n t i n u e s r i g h t a f t e r t h o s e 3 l o n g words , so jump o v e r them / . jmp multiboot entry / A l i g n t h e m u l t i b o o t head er a t a 32 b i t s b o u n d a r y . .align 4 / s t a r t / s t a r t i s t h e g l o b a l e n t r y p o i n t i n t o k e r n e l /

m u l t i b o o t h e a d e r : / Now comes t h e m u l t i b o o t h e a d e r . / .long MULTIBOOT HEADER MAGIC .long MULTIBOOT HEADER FLAGS .long (MULTIBOOT HEADER MAGIC + MULTIBOOT HEADER FLAGS) multiboot entry : / We do not l i k e a s s e m b l y . A l l we want i s t o c r e a t e a a s t a c k and s t a r t e x e c u t i n g C code / / I n i t i a l i z e t h e s t a c k p o i n t e r ( d e f i n i t i o n o f s t a c k f o l l o w s ) / movl $ ( s t a c k + STACK SIZE ) , %esp / Now e n t e r t h e C main f u n c t i o n (EXT C w i l l h a n d l e t h e u n d e r s c o r e , call loop : EXT C( cmain ) i f needed )

/ H a l t . When we r e t u r n from C, we end up h e r e . / hlt jmp loop

. s e c t i o n " .bss " / We d e f i n e our s t a c k a r e a . The .comm pseudo op d e c l a r e s a common symbol (common means t h a t i f a symbol w i t h t h e same name i s d e f i n e d i n a n o t h e r o b j e c t f i l e , i t can be t h e y can be merged ( a f t e r a l l , we o n l y need one s t a c k ) . I f t h e l o a d e r ( l d ) does not f i n d an e x i s t i n g d e f i n i t i o n , i t w i l l a l l o c a t e STACK SIZE b y t e s o f u n i n i t i a l i s e d memory. / .comm s t a c k , STACK SIZE

In the listing, we see that a program consists of three main segments. The text segment contains the executable code, the data segment contains data, and the bss segment contains zerolled static variables. Given this, there is one more thing that we need to specify before we can start compiling and running our code: the locations in memory where we want the linker to place our segments. For instance, should the text segment start at physical address 0x100000, physical address 0x200000, or somewhere else entirely? The same should be answered for the data and bss segments. In addition, we must specify how the segments should be aligned and we must inform the linker that start is the entry point of our code. The linker is responsible for combining dierent output les (such as boot.S and main.c). By

1.4. A WORLD KERNEL (WITHOUT HELLOS)

means of a linker script, we can specify for each of the segments address, size, alignment, etc. Without much explanation, we now give a linker script for our example kernel (see Figure 1.4). The entry point is indicated by the start symbol. We dene our (physical) start address to be 0x00100000 (1M). Thus, the text segment starts at physical address 0x00100000 which is aligned at page size (4KB). A bit above the text segment starts our data segment4 and a bit above that we nd our bss segment. Listing 1.3: link.ld: Linker script for simple kernel
ENTRY( s t a r t ) phys = 0 x00100000 ; SECTIONS { . t e x t phys : AT( phys ) { code = . ; ( .text ) ( . r o d a t a ) . = ALIGN( 4 0 9 6 ) ; } .data : AT( phys + ( d a t a code ) ) { data = . ; ( .data ) . = ALIGN( 4 0 9 6 ) ; } . b s s : AT( phys + ( b s s code ) ) { bss = . ; ( . b s s ) . = ALIGN( 4 0 9 6 ) ; } end = . ; }

With the linker script we have all the pieces of our puzzle. Compiling the code is straightforward and so is running our shiny new kernel under Qemu. Since we will be doing a lot of compiling in the course of this text, we will use a Makele, to capture the compilation commands. For our code, the Makele below will do. Listing 1.4: Makele
CFLAGS all : := fno s t a c k p r o t e c t o r fno b u i l t i n n o s t d i n c O g Wall I . kernel.bin

kernel.bin : boot.o main.o l d T l i n k . l d o k e r n e l . b i n b o o t . o m a i n . o @echo Done ! clean : rm f . o . b i n

You may wonder what all the options are that are specied in CFLAGS. As you probably know, CFLAGS are the options that are passed to the C compiler. In this case, we specify that we do not want gcc to muck around with our addresses much, that we are not interested in all sorts of libraries and that we want to compile it with some (but not much) optimisation and with all debugging symbols present. Table 1.1 briey explains the exact meaning of the various options. At this point, we are solely interested in building the kernel and running it. Assuming all code and the core.img le are in the same directory, we simply execute the following:
make mcopy k e r n e l . b i n c : / b o o t / grub / qemu hda c o r e . img

You may not be very impressed as you do not actually see anything. But really, we just made the most important step of all. We are executing our new kernels code that we programmed in C! All we need now is some I/O functions to allow our kernel to send its greetings to the world.
4 data - code means the oset of the data segment relative to the text segment, as code is dened as pointing to the start of the text segment.

8
option -fno-stack-protector -fno-builtin

CHAPTER 1. A BASIC KERNEL


meaning Do not emit extra code to check for buer overows, such as stack smashing attacks. Dont recognize built-in functions that do not begin with builtin as prex. Gcc normally treats certain built-in functions (like memcpy, and printf) dierently to make the resulting code smaller and faster. This option makes it easier to set breakpoints. Do not search the standard system directories for header les. Only the directories you have specied with -I options (plus that of current le). Optimise compilation (makes code run faster) Produce debugging information in the OSs native format Enables all warnings about constructions that some users consider questionable, and that are easy to avoid (or modify to prevent the warning), even in conjunction with macros. Also look for header (include) les in the current directory.

-nostdinc -O -g -Wall

-I.

Table 1.1: gcc options explained

1.5

Hello World!

The problem with I/O is that functions like printf are not supported in our trivial kernel. If we want to print things on the screen, we will have to write all the required support ourselves. For simple, console-based IO with VGA, this is not very hard to do. In this section we will add minimal support functions to make a programmers life easier and to allow for printing on screen. Since we are now in C, adding functions itself is straightforward. For instance, let us start by adding a few memory manipulation functions in a separate le mem.c. These functions are so simple that we simply give them without any explanation. We will see shortly why we need these functions. At any rate, they are good functions to have. Listing 1.5: mem.c: simple memory functions
/ Convenient f u n c t i o n s f o r m a n i p u l a t i n g memory . We do not have s t a n d a r d l i b c f u n c t i o n s , so we must implement e v e r y t h i n g o u r s e l v e s / unsigned char memcpy ( unsigned char d e s t , const unsigned char s r c , i n t c o u n t ) { int i ; f o r ( i =0; i <c o u n t ; i ++) d e s t [ i ]= s r c [ i ] ; return d e s t ; } unsigned char memset ( unsigned char d e s t , unsigned char v a l , i n t c o u n t ) { int i ; f o r ( i =0; i <c o u n t ; i ++) d e s t [ i ]= v a l ; return d e s t ; }

1.5.1

Basic I/O

Memory copies and initialisations are all well and good, but it is I/O we are aiming for. Our basic strategy is to provide two fundamental I/O functions (inportb() and outportb()) that allow us to read and write I/O ports. For instance, we may use inportb to read a single byte from the keyboard, and outportb to write a single byte to the screen. These fundamental functions map exactly on two fundamental instructions in the x86 instruction set for performing I/O: inb and outb. Unfortunately, these instructions are only available from assembly, but the assembly is really simple (one instruction) and can be easily wrapped in a C function. Listing 1.5.2 shows how this is implemented in C using a (tiny) bit of inline assembly. Listing 1.6: basicio.c: the primitive I/O functions inbyte and outbyte
/ We use i n b y t e f o r r e a d i n g from t h e I /O p o r t s t o g e t d a t a from d e v i c e s such as t h e k e y b o a r d . To do so , we need t h e i n b

1.5. HELLO WORLD!


i n s t r u c t i o n , which i s o n l y a c c e s s i b l e from assemby . So t h e C f u n c t i o n i s s i m p l y a wrapper around a s i n g l e a s s e m b l y instruction . / uint8 t inbyte ( u i n t 1 6 t port ) { uint8 t ret ; asm volatile ( " i n b %1 , % 0 " : " = a " ( r e t ) : " d " ( p o r t ) ) ; return r e t ; } / We use o u t b y t e t o w r i t e t o I /O p o r t s , i . e . , t o send b y t e s t o d e v i c e s . Again , we use i n l i n e a s s e m b l y f o r t h e s t u f f t h a t cannot be done i n C. / void o u t b y t e ( u i n t 1 6 t { asm volatile } port , u i n t 8 t data ) : " d " ( port ) , " a " ( data ) ) ;

( " o u t b %1 , % 0 " :

Inline assembly is easily worth a book in its own right5 , but in this chapter, we limit ourself to fairly mundane usage. In the rst function, we see that the instruction inb is called with two parameters. The second argument (right after the rst colon) lists all the output registers. In this case, the return value ret will be returned in the a register (the =a constraint means that the return valie will be in eax). As input (specied after the second colon) it takes an operand in the d (which indicates edx) register and which represents an I/O port. The outbyte function works similarly. It does not have output operands, but takes two inputs: the I/O port (in register edx) and a data byte (in the eax register).

1.5.2

Writing to screen

We will show how the above two functions, inbyte and outbyte help us print characters on the screen. We limit ourselves to fairly simple VGA-based output. VGA stands for Video Graphics Array and represents an interface between a computer and its corresponding monitor. The VGA card is easily the most common video card, supported by almost any video card. Better still, it is really easy to program VGA in (colour) text mode. In a nutshell, the card oers an area of memory that starts at address 0xB80006 to which we can simply write characters consisting of two byte values: the character itself is in the lower byte, while an attribute byte is the highest byte. The attribute byte represents the background colour in its most signicant four bits and the foreground colour in its least signicant four bits. You can play with these colours yourself. For example: black is 0x0, and white is 0xF, so we can print a black on white B by storing the value (0xF0<<8)|B at the appropriate position in the memory area. The memory area itself is at but represents an 80x25 matrix of these 16-bit characters laid out as 25 consecutive lines. The origin (0, 0) represents the top left corner. The rst 80 characters (of 16b each)represent the top line. The next 80 characters represent the next line, and so on. This means that if we want to print a character at position (x, y ), we have to calculate the oset from the base address as follows: of f set = y 80 + x. We can print a black B on a white background at location (1, 2) as follows:
#d e f i n e VGA START 0 xB8000 u i n t 1 6 t addr = VGA START + 2 80 + 1 ; addr = ( 0 xF0 << 8) | B

Rather than printing each character individually, we will write a few functions to facilitate the printing of strings and other values. The code is extremey straightforward and we give it without further explanation.
5 In fact, the web hosts several long tutorials on the topic, such as http://www.ibiblio.org/gferg/ldp/ GCC-Inline-Assembly-HOWTO.html and http://www.ibm.com/developerworks/linux/library/l-ia.html 6 VGA oers other memory areas as well. For instance, if you want to use graphics, you want to use the area that starts at 0xA0000 (and set the card to mode 0x13),

10

CHAPTER 1. A BASIC KERNEL Listing 1.7: scrn.c: Functions for printing to screen

#include " t y p e s . h " // u i n t 1 6 t , e t c . #include " m e m . h " // memcpy and memset #include " b a s i c i o . h " // i n b y t e and o u t b y t e #d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e COLOURS 0xF0 COLS 80 ROWS 25 VGA START 0 xB8000 PRINTABLE( c ) ( c>= ) / i s

t h i s a p r i n t a b l e c h a r a c t e r ? /

u i n t 1 6 t S c r n ; // s c r e e n area i n t Curx , Cury = 0 ; // c u r r e n t c u r s o r c o o r d i n a t e s u i n t 1 6 t EmptySpace = COLOURS << 8 | 0 x20 ; / 0 x20 i s // s c r o l l t h e s c r e e n ( a copy and b l a n k o p e r a t i o n ) void s c r o l l ( void ) { i n t d i s t = Cury ROWS + 1 ; if

a s c i i v a l u e o f s p a c e /

( d i s t > 0) { u i n t 8 t n e w s t a r t = ( ( u i n t 8 t ) S c r n ) + d i s t COLS 2 ; i n t bytesToCopy = (ROWS d i s t ) COLS 2 ; u i n t 1 6 t n e w b l a n k s t a r t = S c r n + (ROWS d i s t ) COLS ; i n t bytesToBlank = d i s t COLS 2 ; memcpy ( ( u i n t 8 t ) Scrn , n e w s t a r t , bytesToCopy ) ; memset ( ( u i n t 8 t ) n e w b l a n k s t a r t , EmptySpace , bytesToBlank ) ;

} } // P r i n t a c h a r a c t e r on t h e s c r e e n void p u t c h a r ( u i n t 8 t c ) { u i n t 1 6 t addr ; // f i r s t h a n d l e a few s p e c i a l c h a r a c t e r s

// t a b > move c u r s o r i n s t e p s o f 4 i f ( c == \ t ) Curx = ( ( Curx + 4 ) / 4 ) 4 ; // c a r r i a g e r e t u r n > r e s e t x pos e l s e i f ( c == \ r ) Curx = 0 ; // n e w l i n e : r e s e t x pos and go t o n e w l i n e e l s e i f ( c == \ n ) { Curx = 0 ; Cury++; } // b a c k s p a c e > c u r s o r moves l e f t e l s e i f ( c == 0 x08 && Curx != 0 ) Curx ; // f i n a l l y , i f a normal c h a r a c t e r , p r i n t i t e l s e i f (PRINTABLE( c ) ) { addr = S c r n + ( Cury COLS + Curx ) ; addr = (COLOURS<<8) | c ; Curx++; } // i f we have r e a c h e d t h e end o f t h e l i n e , move t o t h e n e x t i f ( Curx >= COLS) { Curx = 0 ; Cury++; } // a l s o s c r o l l scroll (); } // p r i n t a l o n g e r s t r i n g void p u t s ( unsigned char s t r ) { while ( s t r ) { p u t c h a r ( s t r ) ; } i f needed

s t r ++;}

void i t o a ( char buf , i n t base , i n t d ) { char p = b u f ; char p1 , p2 ; unsigned long ud = d ;

1.5. HELLO WORLD!


int d i v i s o r = 10; / I f %d i s s p e c i f i e d and D i s minus , p u t i n t h e head . i f ( b a s e == d && d < 0 ) { p++ = - ; b u f ++; ud = d ; } e l s e i f ( b a s e == x ) divisor = 16; / D i v i d e UD by DIVISOR u n t i l UD == 0 . do { i n t r e m a i n d e r = ud % d i v i s o r ; / /

11

p++ = ( r e m a i n d e r < 1 0 ) ? r e m a i n d e r + 0 : r e m a i n d e r + a 1 0 ; } while ( ud /= d i v i s o r ) ; / Terminate BUF. p = 0 ; / Reverse BUF. / p1 = b u f ; p2 = p 1 ; while ( p1 < p2 ) { char tmp = p1 ; p1 = p2 ; p2 = tmp ; p1++; p2 ; } } // Format a s t r i n g and p r i n t i t on t h e s c r e e n , // f u n c t i o n p r i n t f . void p r i n t f ( const char format , . . . ) { char a r g = ( char ) &f o r m a t ; int c ; char b u f [ 2 0 ] ; a r g ++; while ( ( c = f o r m a t++) != 0 ) { i f ( c != % ) putchar ( c ) ; else { char p ; c = f o r m a t ++; switch ( c ) { case d : case u : case x : i t o a ( buf , c , p = buf ; goto s t r i n g ; break ; just like the libc /

( ( i n t ) a r g ++));

case s : p = a r g ++; i f ( p == NULL) p = "( null )" ; string : while ( p ) p u t c h a r ( p++); break ; default : p u t c h a r ( ( ( i n t ) a r g ++));

12
break ; } } } } // C l e a r t h e s c r e e n void c l e a r ( ) { int i ; f o r ( i = 0 ; i < ROWSCOLS ; Curx = Cury = 0 ; // Scrn [ i ] = EmptySpace ; } // i n i t and c l e a r t h e s c r e e n void v g a i n i t ( void ) { S c r n = ( unsigned short )VGA START ; clear (); }

CHAPTER 1. A BASIC KERNEL

i ++) p u t c h a r ( ) ;

We wrap up our very basic kernel with a simple main function. It prints its greeting and starts an endless loop. It still is not much, but we do have a kernel with primitive I/O. In the next few chapters we will add (more interesting) drivers and other interesting features to our basic kernel. Listing 1.8: main.c: simple main function
void cmain ( unsigned long magic , unsigned long addr ) { vga init (); puts ( ( u i n t 8 t ) " hello world ! " ) ; f o r ( ; ; ) ; // s t a r t i n f i n i t e l o o p }

We are done with the rst chapter. Assuming that we saved the main function in main.c, memset and memcopy in mem.c, inbyte and outbyte in basicio.c, vga functions in scrn.c, and bootstrap code in boot.S and that we included the appropriate header les everywhere, we can now simply build the kernel and boot it. Here is the makele: Listing 1.9: Makele: hello world with VGA output
CFLAGS := fno s t a c k p r o t e c t o r fno b u i l t i n n o s t d i n c O g Wall I . all : k e r n e l . bin k e r n e l . bin : b o o t . o main . o mem. o b a s i c i o . o s c r n . o l d T l i n k . l d o k e r n e l . b i n b o o t . o main . o mem. o b a s i c i o . o s c r n . o @echo Done !

Bibliography
[1] F. Bellard. Qemu, a fast and portable dynamic translator. In USENIX ATEC 05, 2005. [2] B. Friesen. Brans kernel development tutorial. http://www.osdever.net/bkerndev/index. php.

13

You might also like