Library Calls

From OSDev Wiki
Jump to navigation Jump to search

This page is under construction! This page or section is a work in progress and may thus be incomplete. Its content may be changed in the near future.

This page or section refers to its readers or editors using I, my, we or us. It should be edited to be in an encyclopedic tone.

#include <stdio.h>
int answer = 42;
printf("The answer is %d!\n", answer);

We all know this construct from our userspace programming experiences. We never really thought about how it worked, at least for some time. You #include, you get to use "the system".

For some, when starting their own OS project, there are some surprises. This page is meant to lessen the confusion, or to avoid it outright: How does a library call actually work, and what is different from kernel space?

Note that the system call procedure described below is pretty close to what Linux actually does. Of course different approaches can be devised.

Language Basics

Compiler

Let us have a look at the above code fragment, again. You could just as well write:

int printf(const char * format, ...);
char * msg = "World";
printf("Hello %s!\n", msg);

The header <stdio.h>, in this case, does serve no other purpose than to tell the compiler that there is a function printf() that takes a parameter list consisting of at least a const char pointer, and returns int. Nothing else is necessary, and mere convenience so you don't have to write your own declarations or #include each function individually.

Linker

The linker is a different story. When the compiler is done compiling your source file into object files, it is up to the linker to link all object files together into an executable. It also resolves symbols. For the above code, the object code refers to a function printf() which is, however, not defined in the object code itself.

It is defined in the system libraries. Luckily the linker knows that these exist, and where to find them. Any symbols not resolved in your own code it tries to resolve using the system libraries. It also links in the startup code, since someone has to set up the environment and call your int main(), right?

GCC Cross-Compiler

If you did follow the GCC Cross-Compiler how-to, your cross-compiler does not know about <stdio.h>, and your cross-linker does not know about any system libraries. Which is just as well, since they don't exist.

At the Bottom

Let us assume that, at the bottom of it, you already have some kernel-space function that accepts a char * as parameter, assumes that this points to a zero-terminated string, and prints it to screen in some way (writing to video mem, calling the BIOS, updating the framebuffer, whatever). We will not bother with other "channels" like writing to file or something.

Userspace printf()

Well, the usual userspace process for a library call is already explained above, is it? Well, yes, but only at a high level. What actually happens when you call printf() like that?

Your userland printf() does not have a string to print, yet - only a parameter that happens to be an integer, and a manual on how to make a string of it. It then proceeds to construct that very string, "The answer is 42!\n". In some way. Doesn't matter.

Then comes the funny part. The function that does the actual printing (see "At the Bottom" above) is in kernel space. You can't just execute kernel functions like that, only the kernel can.

So let's say your standard library implementation of printf(), after assembling the string to be printed, calls a function named write(), passing it a pointer to the readily-assembled string and the information that it is to be printed to stdout - like, the integer 1.

write() now does something special: It places a special code into one register, the char * and the integer in some others, and calls an interrupt.

Enter the kernel

An interrupt ends userspace processing, and wakes up the kernel - more specifically, the interrupt handler registered by the kernel. Since it was the interrupt reserved for system calls, it knows what to do: The special code tells it that it was write() calling the interrupt. That means that there is a char * in this register, and an int in that... oh, it's a 1, so this goes to stdout (instead of, say, a file).

So the interrupt handler passes the char * to the printing function we mentioned in "At the Bottom" above, then returns control to the caller.

Done

The interrupt handler ends, write() is back in control, which returns to printf() which returns to your application. You successfully completed a call to printf().

Kernel is Different

If you are in kernel space anyway, you don't want that hassle with the registers and the interrupt. After all, you can call the print function directly as you are in kernel space. You also never want to write to a file. The write() function and the interrupt handler become unnecessary baggage.

And you probably don't want all the code necessary for e.g. floating point conversions or unicode output linked into your kernel binary, either. The userspace printf() is much too heavy for your tastes.

So what do you do? The answer is easy. Just tell the linker that the _system libraries_ are to be found in a different directory than for userspace. (Your Makefile is already loaded with funny options for compiler and linker, one more wouldn't make a difference. Bad pun warning.)

So you provide a lightweight printf() for kernel space. You don't even have to bother to call it kprintf() or something as your kernel binary will never be linked with userspace code. Using some preprocessor #ifdef magic, you can even use the very same <stdio.h> header file as for userspace code, reducing redundancy and potential error sources: The preprocessor symbol __STDC_HOSTED__ (with two leading and trailing underscores that the Wiki stubbornly interprets as boldface markup) is undefined when you set -ffreestanding...

#ifdef __STDC_HOSTED__
// Userspace declarations
#else
// Kernelspace declarations
#endif