Library Calls

From OSDev.wiki
Revision as of 03:26, 7 March 2007 by osdev>Jhawthorn
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Template:Convert

!! Introduction

#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=...

<verbatim>

  1. ifdef __STDC_HOSTED__

// Userspace declarations

  1. else

// Kernelspace declarations

  1. endif

</verbatim>