DLLs for FNK

This sort of outlines my process of design for DLLs when creating the FNK operating system. I'll continue adding onto this post until the system is done, but I will also extrapolate some of the designs that I haven't implemented yet. Basically, the content here is up for change.


Background + Motivation

Its important to understand things like what is a DLL in programming and how processes are even loaded.

The need for dynamic

Nearly all programs will attempt to access some form of data. Whether that be through register displacement: mov ax, [bp + label], relative addressing: jmp short +5 or absolute means: mov ax, [label] your program needs to access data. We can already see a problem when it comes to the absolute means: what happens if our program is put somewhere else? That label marker is relative to literally nothing and is bound by how it is compiled. Once the program is compiled the code will flop down to mov ax, [0x55AA]. Say I place my program in memory at 0x55AA even thought it was expected to be loaded in at 0x3333, I'm now accessing data improperly. With a process system, you want to be able to fit as many programs as possible and give them the room they need. But if I need to specify where processes are loaded, then processes could possibly overlap or request the same memory origin. Personally, I just didn't want to have to make the user manage their own process's memory. Now, you might be thinking "well why don't we just only use relative addressing (and/or displacement to registers like bp)?" Well first of all, someones a bit demanding of their compiler. Still, in a modern context something like that might be possible. But there are still multiple reasons why this wouldn't work (besides the pain and suffering of compiler writers):

Modern processors have devised nice solutions to this like memory segmentation which makes programs think that they are loaded in at a specific memory location virtually (but physically their at a different address) but I don't want to have to deal with any architecture-specific memory manager, so I opted for a different means.

The need for libraries

Libraries are pieces of code that serve a specific purpose. They are entirely necessary for my project due to the fact that I things like syscalls and util code was necessary. I wasn't going to write every program from scratch and I had to create some non-jank way to communicate to the operating system and drivers. I was going to use sockets of course to communicate to drivers, but I also wanted to be able to get that socket and do other operations via some other way. I had two options here: DLL or statically linked library

Because many RTOSs don't have dynamic process loading (I need this because you can insert a chip and that creates a new process) the static solution is good enough. They simply shove everything together so there will only be 1 instance of every library. Programs know where the libraries are because they are linked with knowledge of the library. Regardless, I opted for DLLs because:


The solution

I devised a simple custom format: .fnk. The format allows me to solve all of my problems with very little processing from the operating system.

Relocation tables

Relocation tables are a beautiful thing. They are simply a table that points to all occurrences of absolute addressing. With this information we can just add an offset (where we load in the program) to all entries in the relocation table. Even better, relocation tables are generated by the compiler. You know when you create an object file with -c? That creates a relocatable ELF file. It includes a relocation table for the program. Why? Because when the final linking process happens and the linker may want to move sections (parts of code are put in sections and then absolute addressing usually includes the section + an offset) around in the final binary, it actually uses that relocation table to change the offsets of those sections. To create our program, we can simply resolve all relocation entries (the relocation table contains the absolute address usually so we have to fill that in) and then write the new table with just pointers and sizes to the binary

for (entry = reloctable; entry < reloctable + sizeof(reloctable); entry++) {
    relocentry_resolve(entry, code); // Write the absolute addresses to the code
    fwrite(relocplace, entry->position, entry->size);
}

And when our operating system loads in a program:

for (entry = reloctable; entry < reloctable + sizeof(reloctable); entry++) {
    code[entry->position] += load_offset;
}

Now something like mov ax, [0x55AA] will become mov ax, [0x55AA + 0x3333] (the addition is evaluated I'm just using it as a representation)

DLLs + processes = good but unsafe

With no memory protection utilization I don't care about safe design decisions. So, I decided to make libraries and processes be the same thing. This is through LJDs and PJDs

LJDs

LJDs (library jump descriptor) is a list of the functions you want to expose. You create pointers to all of those functions and then another program can copy it over and reference whatever it wants to.

ljd[] = {
   function_one,
   function_two,
   function_three
};

When the program is loaded this LJD is modified to include the program offset.

PJDs

PJDs (program jump descriptor) are where the LJDs are copied to. The program will look through all libraries listed in the PJDs and then copy the LJDs from the necessary program. Then all the program has to do is call functions from this array.

struct pjd {
    char name[COMMON_FNKCONFIG_NAMELEN];
    uintptr_t functions[COMMON_FNKCONFIG_MAX_LIBFUNCTIONS];
};

struct pjd fnk_pjd {
   .name = "fnk",
}

void (*fnk_socket_init)(
    struct fnk_socket*,
    void*,
    size_t,
    void*,
    size_t
) = fnk_pjd.functions[0];

Loading

Now all we have to do is a memcpy from the LJD to the PJD for the respective library.

The final product

Here is the file format I devised for the .fnk format with all that in consideration

Parameter Size (bytes) Value description
MAGIC 6 0xFE 0xED 'F' 'N' 'K' 'Y' - Used to confirm .fnk format.
NAME 12 Name of your program. Must be unique.
RELOCATION-ADDR ARCH_CONFIG_WORDSIZE (config.h) Address to the relocation table (0 for none)
ENTRY-ADDR ARCH_CONFIG_WORDSIZE Address of the entry section (0 for none and therefore unexecutable)
LJD-ADDR ARCH_CONFIG_WORDSIZE Address of the LJD section
PJD-ADDR ARCH_CONFIG_WORDSIZE Address of the PJD section
BSS-LENGTH ARCH_CONFIG_WORDSIZE How long the blank storage section will be