Implement Your Own Operating System (Part 06)
The application logic is meant to be handled by the programs, not the kernel. To make application development easier, the kernel generates appropriate abstractions (for memory, files, and devices), executes activities on behalf of applications (system calls), and schedules processes.
In contrast to kernel mode, user mode is the environment in which the user’s programs run. This environment has fewer privileges than the kernel, thus (badly constructed) user programs will not be able to interfere with other programs or the kernel. Kernels that are poorly written are free to do anything they want.
Although there is still a long way to go before the OS built in this book can run applications in user mode, this article will demonstrate how to run a tiny program in kernel mode.
Loading an External Program
Where do we get the external program from? Somehow we need to load the code we want to execute into memory. More feature-complete operating systems usually have drivers and file systems that enable them to load the software from a CD-ROM drive, a hard disk, or other persistent media.
Instead of creating all these drivers and file systems, we will use a feature in GRUB called modules to load the program.
Ok, now we will implement step by step our simple program,
1. GRUB Modules
GRUB can load arbitrary files into memory from the ISO image, and these files are usually referred to as modules. To make GRUB load a module, edit the file iso/boot/grub/menu.lst
and add the following line at the end of the file:
module /modules/program
Now create the folder iso/modules
:
mkdir -p iso/modules
The application program
will be created later in this chapter.
The code that calls kmain
must be updated to pass information to kmain
about where it can find the modules. We also want to tell GRUB that it should align all the modules on page boundaries when loading them.
To instruct GRUB how to load our modules, the “multiboot header” — the first bytes of the kernel — must be updated as follows:
GRUB will also store a pointer to a struct
in the register ebx
that, among other things, describes at which addresses the modules are loaded. Therefore, you probably want to push ebx
on the stack before calling kmain
to make it an argument for kmain
.
2. Executing a Program
A program written at this stage can only perform a few actions. Therefore, a very short program that writes a value to a register suffices as a test program. Halting Bochs after a while and then check that register contains the correct number by looking in the Bochs log will verify that the program has run. This is an example of such a short program:
3. Compiling
Since our kernel cannot parse advanced executable formats we need to compile the code into a flat binary. NASM can do this with the flag -f
:
nasm -f bin program.s -o program
This is all we need. You must now move the file program
to the folder iso/modules
.
4. Finding the Program in Memory
Before jumping to the program we must find where it resides in memory. Assuming that the contents of ebx
is passed as an argument to kmain
, we can do this entirely from C.
The pointer in ebx
points to a multiboot structure:
The pointer passed to kmain
in the ebx
the register can be cast to a multiboot_info_t
pointer. The address of the first module is in the field mods_addr
. The following code shows an example:
int kmain(/* additional arguments */ unsigned int ebx)
{
multiboot_info_t *mbinfo = (multiboot_info_t *) ebx;
unsigned int address_of_module = mbinfo->mods_addr;
}
However, before just blindly following the pointer, you should check that the module got loaded correctly by GRUB. This can be done by checking the flags
field of the multiboot_info_t
structure. You should also check the field mods_count
to make sure it is exactly 1. For more details about the multiboot structure, see the multiboot documentation [19].
5. Jumping to the Code
The only thing left to do is to jump to the code loaded by GRUB. Since it is easier to parse the multiboot structure in C than assembly code, calling the code from C is more convenient (it can of course be done with jmp
or call
in assembly code as well). The C code could look like this:
typedef void (*call_module_t)(void);
/* ... */
call_module_t start_program = (call_module_t) address_of_module;
start_program();
/* we'll never get here, unless the module code returns */
After done above changes kmain.c
file:
If we start the kernel, wait until it has run and entered the infinite loop in the program, and then halt Bochs, we should see 0xDEADBEEF
in the register eax
via the Bochs log. And also If all are fine you can see the characters which you typed in the terminal in com1.out file.
Well done! We have successfully started a program in our OS!. We will meet the next article Implement Your Own Operating System (Part 07).
Completed project: Github
Reference: The Little OS Book
Thank you!