FNK

FNK is a light C99 operating system frame that targets multiple architectures (particularly embedded). <-- The design constraints!
It is a toy but it is also meant to be a very serious and scalable project. It definitely challenges my design skills and I'd honestly consider it to be some kind of magnum opus. It takes a lot of inspiration from the Linux kernel, as a lot of my design values are from there. Overall, as a programmer I think I've significantly improved from working on this project, not only at design, but recognizing and prioritizing maintainable code


Design preferences

I'm not going to be talking about the build system here because I honestly think that deserves its own thing. Here I'm just going to talk about the many things I did in order to create maintainable code that is meant for multiple architectures and platforms.

Modules

If you want you can look at my more in depth post about modularity and its importance here
TL;DR I think that separating code into architecture and platform specific code makes much more maintainable code than the alternatives. In this project I implemented a few libraries to support multiple architectures (others like kernel, DLLs not mentioned)

The common module is where I put a lot of utility functions like memcpy, memset, etc. Its actually compiled twice because I also use it in some of the native code.

The arch/$(ARCH) module (where $(ARCH) is the target architecture) contains user-written code. I made it so the user is in charge of completing their architecture specific code because I didn't want to write a ton of drivers (of course), but I also didn't want to delve into the land of bloat. I'm aiming for a lightweight framework, not a heavy hitting compatible monster. I also aimed for minimal architecture specific code. I just needed some types to get off the ground and, drivers, and of course the enter and exit functions + stack operations for the process system.

The comp/$(COMP) module (where $(COMP) is the compiler) contains more user-written code! Thankfully, a lot of compilers try to be more standardized (I love you GCC) but it's better to be safe by having some specific things defined.
SIDE-TANGENT: I love C but this language is so bad sometimes. Bitfields, having no packed structures, alignment, etc, are all things the language LACKS. WHY??? It is such a good language otherwise but these small things are so annoying. THIS SHOULD BE STANDARD. While I understand things like the need for int, not requiring a compiler that supports at least ALIGNING a variable is doing NOTHING good for the language! Or even something simple like noreturn
Side tangent over. This is a necessary module but some of the stuff that needs to be recorded here is preposterous.


Build

You can read more here. The FNK build system is actually pretty in depth. It's written entirely in makefile (you could argue that the binary conversion tool is written in C). It may not be the fastest but I do feel like it's very maintainable and scalable.

Recursive Makefiles

Say what you want about performance but recursive Makefiles are a godsend for design. While I don't know which project devised them, the Linux kernel was the project I took heavy inspiration from. If you don't know a recursive makefile is a makefile which calls other Makefiles inside of its instance. Theres a number of reasons why this is great: code reuse, module flexibility, dependencies through modules etc.

Mirrored output

I haven't implemented this yet. This is another thing I found from the Linux kernel. Instead of outputting object files to the same directory, linux will create another directory(s): out/. This out directory includes a mirror of the root directory and is where all of the object files and library goes. The good thing about this is it works great for multiarch designs. By making multiple output directories (e.g. out-target/ out-native/), we can specify via makefile targets in order to indicate the compiler we want to use. Furthermore, this makes it so the native and target code are never mixed around.


Sockets

You can read more here. To do hardware and process communication, I opted for sockets: a two way double buffered system. By using ring buffers to make both a read and write buffer, we can allow data transmission. Furthermore, by using a socket server, a driver can allocate sockets for processes and tend to those sockets recursively.

Why?

I've always thought that the way we interface with hardware has been extremely unstandardized. Sockets have always been a good means of communication but other devices omit them entirely. As data is usually streamed from device to device, I thought that an easily maintainable system would be to use sockets. The driver can handle all of the intrinsics and the user just has to write data to the sockets. They're also completely generic which makes them quite easy to use.


DLLs

You can read more here. Because I wanted to allow ROMs to reuse library code, I sought out the creation of DLLs (dynamically linked libraries).

How do you dynamically link?

When you load in a program you can't just put it at a fixed location (unless your architecture supports paging but I don't want the user to have to support that). And if you don't put it at a fixed location, the program will try to reference something absolutely and that will grab memory we don't want it to. To combat this, we modify all occurrences of absolute addressing by adding the memory location the program was loaded into to those addresses. When you compile with -c you create whats called a relocatable elf file. These files include a relocation table which exposes the location of occurrences of absolute addressing.

.fnk format

Having binaries be relocatable elf files instead of just a custom format is good idea. ELF is generally a big format because it aims to support Linux (a lot of possible run configurations could happen). For us, we just need a bit of information:

  • Relocation table
  • Where is the entry
  • Special sauce for linking

I devised this format:

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

This gives us access to the relocation table and more, while still giving us access to the other things we need. I'd recommend reading my article linked above because I can't explain this all in just a section.