Table of Contents
GeekOS is an operating system kernel. In many respects, it is simply a C program. It has functions, threads, a memory allocator, and so forth. However, unlike a C program executing as a user mode process of a host operating system such as Linux or Windows, a kernel operates in kernel mode. A program executing in kernel mode has total control over the computer's CPU, memory, and hardware devices.
Writing code to execute in kernel mode presents a few challenges that you will need to be aware of. This chapter presents some important tips and techniques that you will need to use as you modify the GeekOS kernel.
The runtime environment of the GeekOS kernel has several important restrictions you will need to be aware of.
Because the operating system is that lowest level of software in the computer, all functionality used by the kernel must be implemented within the kernel. This is different than for user programs, which are generally linked against a set of standard libraries containing often-used functions.
The only standard C library functions available in GeekOS are a subset of the string functions (strcpy(), memcpy(), etc.) and the snprintf() function. Prototypes for these functions are defined in the header <geekos/string.h>.
In addition to the standard C functions, the GeekOS kernel also contains functions which are similar to C library functions. The Print() function (prototype in <geekos/screen.h>) is a subset of the standard C printf() function. The Malloc() and Free() functions (prototypes in <geekos/malloc.h>) are equivalent to the malloc() and free() functions.
Each thread in the GeekOS kernel has a 4K stack. If a thread overflows its stack, a kernel crash will generally be the result. Therefore, you should be very careful to conserve stack space:
Do not allocate large data structures on the stack. Use the kernel heap allocator (Malloc() and Free()) instead.
Do not use recursion. Avoid deep call stacks.
When the kernel starts executing there is no memory protection; every memory access made by the kernel is to real (physical) memory. Dereferences of null pointers are not trapped. For this reason, kernel code is more vulnerable to stray pointer references than user code. You will need to check your code very carefully to make sure there are no memory errors, because they are hard to debug.
When you add virtual memory to GeekOS (Chapter 9, Project 4: Virtual Memory), the kernel will gain a greater degree of protection for memory errors, but you will still need to be careful.
Many hardware devices use interrupts to notify the CPU of important events: the expiration of a timer, the completion of an I/O request, etc. An interrupt is an immediate asynchronous transfer of control to an interrupt handler. When the handler completes, control returns to the point where execution was interrupted, and the original code resumes. Note that interrupt handlers may cause a thread context switch, meaning that other threads may execute before control returns to the interrupted thread.
The practical implications of interrupts are described in Section 2, “Interrupts and Threads”. You will want to read this section carefully.
If you observe the precautions described in this document, you will find that programming in kernel mode is fairly straightforward. However, to make your kernel hacking experience more pleasant, we strongly encourage you to adopt the following habits.
The header <geekos/kassert.h> defines the KASSERT() macro, which takes a boolean expression. If the expression evaluates to false, the macro prints a message and halts the kernel. Here is an example:
void My_Function(struct Thread_Queue *queue)
{
KASSERT(!Interrupts_Enabled()); /* Interrupts must be disabled */
KASSERT(queue != 0); /* queue must not be null */
...
}
As much as possible, you should rigorously use assertions to check function preconditions, postconditions, and data structure invariants. Assertions have two important benefits. First, a failed assertion immediately and precisely pinpoints a bug in the code, often before the kernel state becomes seriously corrupted. Second, assertions help document the code.
The <geekos/screen.h> header defines the Print() function, which supports much of the functionality of the standard C printf() function. Print statements are the most useful general technique for debugging kernel code. As with any debugging, you should adopt the strategy of forming a hypothesis and gathering evidence to support or refute the hypothesis.
To a much greater extent than for user programs, kernel code needs to be developed and tested in small pieces. Whenever you reach a stable point in your kernel development, you should commit your work to version control. We strongly recommend that you use CVS to store your code.