JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Linking and Loading: From Source to Running Image

The last three guides asked where a process lives in RAM and how its addresses get translated. This one rewinds further: how does the pile of code you typed even become a running image with addresses at all? Meet the linker that stitches the pieces together and the loader that drops the result into memory.

A program is made of pieces

So far this rung has treated a process as if it just appeared in memory, fully formed, ready for the MMU to translate its addresses. But where did that finished image come from? Real programs are not written as one giant file. You write your code in several source files; a library someone else wrote handles printing and math; the system provides yet more code for talking to the disk. Each of these is compiled separately into an object file — a chunk of machine code that is almost ready to run, but not quite, because it keeps referring to things that live in the other pieces.

Think of building a piece of furniture from a kit. Each bag of parts is complete on its own, but the instructions say things like "attach panel A to the bolt from bag 3." Bag 1 cannot finish the job alone; it has a hole that only bag 3 can fill. An object file is exactly like that: when your code calls a function named `printf`, the compiler does not yet know where `printf` will sit in memory, so it leaves a labelled blank — an unresolved reference — that says "jump to wherever printf ends up." Every object file is a mix of finished machine code and these labelled blanks pointing at symbols defined elsewhere.

The linker: stitching the pieces and filling the blanks

The job of taking all those object files (plus the library pieces they need) and welding them into one coherent program belongs to the linker. It does two things. First, it lays the pieces out one after another into a single combined image, deciding the relative position of each chunk of code and data. Second — the crucial part — it walks through every labelled blank and fills it in: now that it knows where `printf` was placed, it can patch the "jump to printf" instruction with printf's actual offset. Resolving these symbols and patching the references is the heart of linking.

Notice what kind of address the linker is filling in. It cannot possibly know which physical RAM the program will land in later — that depends on what else is running when the user double-clicks it. So the linker works in logical addresses: it produces an image laid out as if it will start at address 0 and run upward, exactly the private world from guide 1. This is the moment in our story where that private numbering is actually created. The linker bakes in offsets that are correct relative to the start of the image, leaving the question of the real starting address for later. That "deciding the real address" question has a name: address binding, and it can happen at three different times.

  1. Compile time: if you knew up front exactly where in RAM the program would run, you could bake the real physical addresses straight in. Some tiny embedded systems do this, but it is rigid — move the program and every address is wrong.
  2. Load time: the linker leaves addresses as offsets from zero, and when the program is brought into memory the loader rewrites each one by adding the chosen starting address. This works, but the rewriting must be redone every time the program is reloaded somewhere new.
  3. Execution time: addresses stay as offsets from zero even while the program runs, and the MMU adds the base register on every single access. This is the scheme this whole rung assumes, because it is what lets the OS relocate a running process freely.

These three binding times are why guide 2's relocation mattered so much. With execution-time binding the program never commits to a real address, so the OS can move it, swap it out, or compact around it just by changing a register — none of which would be possible if the linker had hard-wired physical addresses at compile time. The whole flexible memory machinery of this rung quietly rests on the linker choosing to leave addresses as offsets rather than baking them in.

The loader: from disk image to live process

The linker hands us a finished executable file sitting on disk — still passive, still just a recipe. Turning it into a live process is the job of the loader, the part of the OS that runs when you launch a program. Recall from the process rung that the exec system call is what replaces a process's contents with a fresh program; the loader is the machinery exec drives. It reads the executable, sets aside RAM for it, copies the code and data into that RAM, and arranges everything the address space needs so the CPU can begin executing the very first instruction.

  on disk (executable)            in RAM (process image)
  +------------------+            base ->+------------------+
  | text (code)      |   loader          | text (code)      |
  | data (globals)   |  ========>        | data (globals)   |
  | bss = zeros      |   copy +          | bss (zeroed)     |
  | symbol/reloc tbl |   set up          | heap   v         |
  +------------------+                   |  ...   (grows)   |
                                         | stack  ^         |
                                         +------------------+
  loader also sets the base register so logical 0 -> base
The loader copies code and data into RAM, zero-fills the bss, leaves room for the heap and stack to grow, and points the base register at the start.

Notice the loader does not copy everything literally. The text (your machine code) and the initialized data are copied straight from the file, but the bss — your global variables that start at zero — is not stored in the file at all; the loader just reserves that much RAM and zeroes it, which keeps the executable smaller. It also leaves empty room for the heap and the stack to grow into at runtime. Finally, if this OS uses load-time binding, the loader walks the relocation table and rewrites every address by adding the chosen base; if it uses execution-time binding, it instead just sets the base register and lets the MMU do that addition forever after.

Static vs dynamic linking, and shared libraries

There is a fork in the road over when the library pieces get folded in, and it is the choice of static versus dynamic linking. With static linking, the linker copies the actual machine code of every library function you use straight into your executable, back at build time. The result is wholly self-contained: one file that needs nothing else to run. The cost is size and staleness. If a thousand programs all statically link the same printing library, a thousand copies of that code sit on disk, and a thousand copies occupy RAM when they all run — and if a bug is found in the library, every one of those programs must be rebuilt to get the fix.

With dynamic linking, the library code is left out of your executable. Instead the linker inserts a tiny stub for each library function: a placeholder that says "the real code lives in a separate library file; go find it." That resolving is deferred until the program actually runs — at load time, or even lazily on the first call. The library it finds is a shared library (a `.so` on Linux, a DLL on Windows), and the magic is in the word shared: a single in-memory copy of that library's code can be mapped into the address spaces of every process using it at once, so all of them read the same physical pages instead of each carrying their own copy.

Putting the pipeline together

Step back and the whole journey from text you typed to code that runs lines up as one pipeline. The compiler turns each source file into an object file full of machine code and labelled blanks. The linker welds the object files and the needed library pieces into one image laid out from logical address zero, resolving the blanks it can. The loader, when you launch the program, copies that image into real RAM, sets up the heap and stack, and either rewrites the addresses or sets the base register so the MMU does it. Only then does a passive program become a living process.

One honest caveat ties this back to the rest of the rung. Everything here still assumes the loader can find one contiguous run of RAM to drop the whole image into — the same contiguous assumption, and the same external-fragmentation pain, from guide 3. On a modern machine that assumption is quietly false: the loader does not need a single block, because the address space is chopped into pages that can scatter across physical memory. But that mechanism — paging — is the next rung's story. The linker and loader stay essential whatever the memory scheme: someone always has to assemble the pieces and place them before the first instruction can run.