Radiant Log #005

Rethinking Program Loading
Alexis Sellier
April 06, 2025

Pondering historical inertia in computing practices..

– Claude (thinking)

This was the readout that flashed for a few seconds when I asked Claude’s reasoning model why we still used such an antiquated process for compiling computer programs. I had to laugh.

Modern operating systems use sophisticated mechanisms for compiling, loading and executing programs that have evolved over the last half a century. Some of these mechanisms were designed around the limitations of computers at the time, in particular the limited amount of RAM, which led to things like separate compilation1.

What I was curious about, as usual, was whether or not there was a simpler way. The answer as it turns out is both yes and no.

The problem

The fundamental problem with program loading and execution is that the processors we use work with absolute memory addresses. This means that when your program calls a function from some external library, the processor has to know the address of this function in memory, so that it can “jump” to it and continue execution there.

Originally, programs were compiled in such a way that all addresses in physical memory were known, by placing all the code at fixed, pre-determined locations in memory. This of course was really inflexible, because not all code is always used, so you eventually end up with lots of bloat and run out of memory.

Position-independent code

Eventually we came up with solutions that made the compilation process simpler and more flexible. On the one side, we started designing instruction sets that used relative addressing, and on the other side we started virtualizing memory. By having the OS map a physical address range to a virtual one, all processes would “see” the same address range without ever overlapping with each other. This made position-independent code; code that can run from any location in memory, easier to achieve.

With this, compilers could use a process called static linking, which involves combining all of the compilation artifacts of a program, including that of external libraries into a single, self-contained executable.

There was one small issue: if you had a set of processes running on a system that all used the same external libraries, you ended up with a lot of duplicated code in memory: each process had its own copy of whatever library it used.

Dynamic linking

Ideally, a single copy of each library in use should reside in memory, and be shared amongst processes that use it. But this would mean that at compile time, our compiler would not know what address to use when emitting function calls to this library, because the code would not be included in the executable, but instead in some location in memory unknown at the time of compilation. The solution to this ended up being dynamic linking, which is the process of computing the memory addresses of external dependencies at runtime.

There is a lot more to this story than I have the time to write about here. In particular, there are many trade-offs between static and dynamic linking, and between separate compilation and whole program compilation.

However, the answer to my original question is that part of the complexity we see in compiler toolchains comes from the way our computer hardware is architected2, and this isn’t likely to change soon.

The other part is accidental complexity: the kind that is accumulated over the years to support this or that OS or architecture; as well as the desire for maximum flexibility. Today’s compiler toolchains have hundreds of knobs, each one of them added for some specific reason. When there is no clear idea of how a tool should be used or for what use it is designed for, it tends to accumulate features for all possible uses. This is the fate of most compilers and operating systems today.

Magic of sharing

I find it incredibly sad that the odds of me compiling a program on my machine, sending it to a friend, and having it work on his machine are slim. One of the reasons for this is dynamic linking, which is the default linking mechanism in most systems. Static linking is often possible, but not always: some libraries cannot easily be statically linked, while others load dependencies at runtime. The AppImage and Flatpak formats offer an alternative, but these require extra work.

In my ideal conception of a computer, sharing programs is easy and commonplace. If I compile a program for a certain CPU architecture and OS, it should run on any such system. It should not require creating a distribution in some central registry nor should it need an installation procedure. The unit of distribution should be the executable itself, and it should work predictably.

Existing solutions

I don’t believe that either static or dynamic linking as seen on modern operating systems is the answer.

Though static linking does provide benefits when it comes to sharing programs, it has two shortcomings that go against the philosophy of Radiant:

  1. It is wasteful, creating more memory pressure than needed.
  2. It is opaque.

The first point is obvious and a common criticism of static linking. The second point is that when we use static linking, we lose the information on the provenance of the modules or libraries used in the executable; in other words, the static linking process is lossy.

So why not use dynamic linking? Dynamic linking has different issues:

  1. It introduces runtime penalties at call sites3.
  2. It introduces load-time penalties3.
  3. It introduces new runtime failure modes (missing shared libraries).
  4. It’s complex to understand and implement.

Conclusion

Looking at the commonly used solutions to program loading leaves me unsatisfied. Though static linking could be a decent compromise, I can’t help but think there is a better way if we’re willing to question some more fundamental assumptions.

What might alternatives look like? One promising direction is exploring capability-based systems, where code references are represented as unforgeable tokens rather than memory addresses. This approach, seen in systems like CHERI, Midori, and EROS, could eliminate many address translation problems while preserving modularity.

For Radiant specifically, I’m drawn to the Oberon system’s module approach, where modules are dynamically loaded but with a simpler and more transparent mechanism than conventional dynamic linking using a single address space, similar to IBM’s System i. Oberon’s module system maintains clear dependencies and type safety across module boundaries while avoiding many of the complexities of modern dynamic linking and virtual memory.

I will therefore continue my research, turning my attention towards these more niche and esoteric techniques that could fulfill the ideals mentioned earlier while keeping the system simple and transparent.


  1. Separate compilation: the process of compiling each source file separately and then linking them together. 

  2. Computer architecture: code is loaded from disk and placed in random access memory (RAM), and processors (CPUs) execute those programs from memory via very basic operations that involve memory reads and writes at physical locations. On the other hand, we humans like to think in terms of symbols, not addresses, and therefore a translation between these symbols and their address in memory is necessary. This explains a big part of the complexity in program linking, loading, and execution. 

  3. Dynamic linking imposes performance penalties at both startup time and runtime. During startup, the system must locate and load all required shared libraries, perform relocations, verify dependencies — processes that can significantly delay program initialization. At runtime, every call to an external function incurs overhead through the PLT/GOT indirection mechanism, where each external function call must go through an extra lookup step rather than jumping directly to the target code.  2