coperating-systemraspberry-pilibrariesc-libraries

Can I just include the C libraries when programming an operating system (since they are made in C)


I'm trying to make an operating system for the Raspberry Pi (nothing big, just for fun) and though I could write it all in Assembly, that would be much harder than writing it in C. I'm wondering if (and why not if I can't) I could just include the C libraries (the files) with the Operating System, so that I wouldn't have to rewrite them. Wouldn't it work because the libraries themselves are written in C?


Solution

  • No, you have to port a C library to your operating system because the library has "stubs" that hook into operating system specifics. The C standard mandates that certain headers are present in freestanding mode, which is always available to you. But library functions like printf have to be implemented yourself or ported by filling in the stubs. Take a look at newlib to see the kind of work you have to do. At the very minimum it requires a working kernel that has a syscall interface (for read, write, etc.) These will depend on the features available in your operating system (for example, a filesystem.) Taken from the FAQ:

    1. What steps do I need to do to port newlib to a new platform?

    A basic port needs to alter a number of files and add some directories.

    1. Add a subdirectory to the newlib/libc/machine directory for your platform

      In this directory you need to have a setjmp/longjmp implementation. This is required because setjmp/longjmp usually is assembler. Look at the libc/machine/fr30 directory and copy/modify the files in there.

    2. Edit newlib/libc/include/machine/ieeefp.h

      This defines the ieee endianness for your platform. The compiler should be defining something that identifies your machine. In some cases, the endianness may be a compiler-option so you may have to check another define in addition to your platform identifier. See examples in the file.

    3. Edit newlib/libc/include/machine/setjmp.h

      You need to specify the setjmp buffer characteristics to match up with your setjmp/longjmp implementation. This is just the size of the setjmp buffer. See file for examples.

    4. Edit newlib/libc/include/sys/config.h

      This has various defines as needed. Mostly, it defines some max values. There are defaults that may apply to your platform in which case you needn't do anything.

    5. Edit configure.host

      You need to add your configuration so newlib can recognize it. You should specify your new machine directory for your platform via the machine_dir variable. If needed, you can add special newlib compile flags. The sys_dir is for OS stuff so you won't need to alter that. Older platforms used the sys_dir to implement syscalls but this is not correct and is a historical nuisance. The syscall_dir is a choice, but I recommend as a default to specify syscall_dir=syscalls. Read the comments in newlib/libc/include/reent.h for an explanation of choices.

    6. Add a platform subdirectory to libgloss

      You need to add a bsp for your platform. This is the minimum set of syscalls needed by newlib and any linker scripts needed. This varies from board to board (it can also be a simulator). See the mn10300 or fr30 for examples. You will need to edit configure.in and regenerate configure so it will build your new files. By default you get libnosys which gives you a set of default syscall stubs. The majority of the stubs just return failure.You still need to supply an __exit routine. This can be as simple as generating an exception to stop the program.

    7. Possibly override header files

      If you need to override any default machine header files, you can add a machine directory to newlib/libc/machine/ Header files in that subdirectory will overwrite the defaults found in newlib/libc/include/machine. You will likely not need to do this.

    This assumes you have already handled adding your new configuration to the top directory files.

    Now linux is a different animal. It is an OS that has an extensive set of syscalls. If you look in the newlib/libc/sys/linux directory, you will find a number of syscalls there (e.g. see io.c). There is a set of basic syscall macros that are defined for the particular platform. For the x86, you will find these macros defined in newlib/libc/sys/linux/machine/i386/syscall.h file. At the moment, linux support is only for x86. To add another platform, the syscall.h file would have to be supplied for the new platform plus some other platform-specific files would need to be ported as well.

    For newlib, take a look at the syscall documentation page which lists what you need to implement and what a minimal implementation consists of. You'll quickly realize that stuff like sbrk will become meaningless if you haven't already implemented memory management. You'll probably end up having written most of your kernel by the time you get to porting a C library.

    _exit

    Exit a program without cleaning up files. If your system doesn't provide this, it is best to avoid linking with subroutines that require it (exit, system).

    close

    Close a file. Minimal implementation:

              int close(int file) {
                return -1;
              }
    

    environ

    A pointer to a list of environment variables and their values. For a minimal environment, this empty list is adequate:

              char *__env[1] = { 0 };
              char **environ = __env;
    

    execve

    Transfer control to a new process. Minimal implementation (for a system without processes):

              #include <errno.h>
              #undef errno
              extern int errno;
              int execve(char *name, char **argv, char **env) {
                errno = ENOMEM;
                return -1;
              }
    

    fork

    Create a new process. Minimal implementation (for a system without processes):

              #include <errno.h>
              #undef errno
              extern int errno;
              int fork(void) {
                errno = EAGAIN;
                return -1;
              }
    

    fstat

    Status of an open file. For consistency with other minimal implementations in these examples, all files are regarded as character special devices. The sys/stat.h header file required is distributed in the include subdirectory for this C library.

              #include <sys/stat.h>
              int fstat(int file, struct stat *st) {
                st->st_mode = S_IFCHR;
                return 0;
              }
    

    getpid

    Process-ID; this is sometimes used to generate strings unlikely to conflict with other processes. Minimal implementation, for a system without processes:

              int getpid(void) {
                return 1;
              }
    

    isatty

    Query whether output stream is a terminal. For consistency with the other minimal implementations, which only support output to stdout, this minimal implementation is suggested:

              int isatty(int file) {
                return 1;
              }
    

    kill

    Send a signal. Minimal implementation:

              #include <errno.h>
              #undef errno
              extern int errno;
              int kill(int pid, int sig) {
                errno = EINVAL;
                return -1;
              }
    

    link

    Establish a new name for an existing file. Minimal implementation:

              #include <errno.h>
              #undef errno
              extern int errno;
              int link(char *old, char *new) {
                errno = EMLINK;
                return -1;
              }
    

    lseek

    Set position in a file. Minimal implementation:

              int lseek(int file, int ptr, int dir) {
                return 0;
              }
    

    open

    Open a file. Minimal implementation:

              int open(const char *name, int flags, int mode) {
                return -1;
              }
    

    read

    Read from a file. Minimal implementation:

              int read(int file, char *ptr, int len) {
                return 0;
              }
    

    sbrk

    Increase program data space. As malloc and related functions depend on this, it is useful to have a working implementation. The following suffices for a standalone system; it exploits the symbol _end automatically defined by the GNU linker.

              caddr_t sbrk(int incr) {
                extern char _end;     /* Defined by the linker */
                static char *heap_end;
                char *prev_heap_end;
    
                if (heap_end == 0) {
                  heap_end = &_end;
                }
                prev_heap_end = heap_end;
                if (heap_end + incr > stack_ptr) {
                  write (1, "Heap and stack collision\n", 25);
                  abort ();
                }
    
                heap_end += incr;
                return (caddr_t) prev_heap_end;
              }
    

    stat

    Status of a file (by name). Minimal implementation:

              int stat(char *file, struct stat *st) {
                st->st_mode = S_IFCHR;
                return 0;
              }
    

    times

    Timing information for current process. Minimal implementation:

              int times(struct tms *buf) {
                return -1;
              }
    

    unlink

    Remove a file's directory entry. Minimal implementation:

              #include <errno.h>
              #undef errno
              extern int errno;
              int unlink(char *name) {
                errno = ENOENT;
                return -1;
              }
    

    wait

    Wait for a child process. Minimal implementation:

              #include <errno.h>
              #undef errno
              extern int errno;
              int wait(int *status) {
                errno = ECHILD;
                return -1;
              }
    

    write

    Write to a file. libc subroutines will use this system routine for output to all files, including stdout—so if you need to generate any output, for example to a serial port for debugging, you should make your minimal write capable of doing this. The following minimal implementation is an incomplete example; it relies on a outbyte subroutine (not shown; typically, you must write this in assembler from examples provided by your hardware manufacturer) to actually perform the output.

              int write(int file, char *ptr, int len) {
                int todo;
    
                for (todo = 0; todo < len; todo++) {
                  outbyte (*ptr++);
                }
                return len;
              }
    

    For a more comprehensive overview of the steps you need to take to port newlib, see osdev.org. Although I recommend reading the other tutorials on the website first that pertain to writing your kernel because porting a C library is absolutely not the first step you take when writing a kernel.