Many object files, one program
At the end of the first guide the assembler handed you an object file: a block of machine instructions plus a table of loose ends. Those loose ends are the names of things this file uses but does not define — a `printf` it calls, a global counter that lives elsewhere. The assembler could not fill in their addresses because it only ever saw one file at a time. A real program, though, is usually many object files plus prebuilt libraries, all compiled separately. Someone has to bring them together and resolve every loose end into a real address. That someone is the linker.
Picture each object file as a chapter written by a different author, full of footnotes like "see the function defined in some other chapter." The linker is the editor who lays all the chapters end to end into one book, then walks every footnote and rewrites it to point at the real page number. Concretely it does two jobs: symbol resolution — matching each unresolved name (a reference) to the one file that defines it — and relocation — once every section has been assigned a final place in the program's address space, patching each instruction and pointer so its numbers point at the right spot. The output is a single executable file where every loose end is tied off.
Static versus dynamic linking
When the linker needs code from a library — say the function that prints to the screen — it has two ways to supply it. Static linking copies the library code straight into your executable, so the finished file is fully self-contained: everything it runs lives inside it. Dynamic linking instead leaves a stub and a note that says "the real `printf` lives in the shared C library; find it at run time." The actual joining-up is then finished not by the linker but by a piece of the loader when the program starts. The whole static-versus-dynamic-linking choice is a trade about where and when the gluing happens.
Each style buys something and costs something. Static linking gives a single fat file with no run-time dependencies — easy to ship, and immune to a library changing under your feet — but every program carries its own copy of the same library, wasting disk and memory, and a security fix in that library means rebuilding every program. Dynamic linking lets dozens of programs share one copy of a library held once in physical memory, and a patched library instantly fixes them all, but it adds a small start-up cost and the infamous risk that the version present at run time is not the one you tested against. Neither is simply better; the choice depends on what you value.
The loader: from file to running process
An executable on disk is just a sleeping pattern of bytes; nothing runs until it is brought to life. That awakening is the job of the loader, a part of the operating system. When you launch a program, the loader reads the executable's header to learn how big each section is, asks the OS for a fresh address space, copies (or memory-maps) the code and initial data into it, sets up an empty stack, finishes any dynamic linking, and finally hands control to the program's first instruction by pointing the program counter at the entry point. From that instant the file on disk has become a living process with its own private view of memory.
- Read the header. The loader parses the executable to learn its sections — code, initialized data, how much zeroed space to reserve — and where each should sit.
- Carve out an address space. The OS hands the new process its own private virtual address space, so it appears to own the whole machine alone.
- Place code and data. The loader maps the program's bytes into that space; thanks to virtual memory it need not copy everything up front and can fault pages in on demand.
- Resolve dynamic libraries. If the program was dynamically linked, the loader finds each shared library, maps it in, and wires up the stubs to the real functions.
- Jump to the entry point. The loader sets the stack and the program counter, then transfers control; your code is now running.
This is also where the OS quietly enforces process isolation. Each process gets its own address space, so one program literally cannot name, let alone touch, another's memory — there is no address it can write that reaches into a neighbour. Process isolation is what lets a single machine run a browser, a music player, and a buggy game side by side, where a crash in one merely ends that one, instead of taking the whole system down with it.
System calls: the guarded doorway to the OS
Your running process is sealed in its own address space, which raises a problem: it cannot read a file, draw on the screen, or send a network packet, because all of those touch hardware shared by everyone. The CPU runs in two privilege levels — an unrestricted kernel mode for the operating system, and a fenced-in user mode for ordinary programs. In user mode the dangerous instructions that talk to devices simply do not work; attempting one traps. So how does your code ever get anything done? It politely asks the OS, through a system call.
A system call is a controlled, deliberate doorway from user mode into kernel mode. The program places a number naming the service it wants (open, read, write, exit) and its arguments into agreed registers — its own little calling convention — then executes a special trap instruction such as RISC-V's `ecall`. That instruction does something no ordinary jump can: it switches the CPU into kernel mode and jumps to a single fixed entry point in the OS, both in one indivisible step. The kernel checks that the request is allowed, performs the privileged work on the program's behalf, puts a result back in a register, and returns control to the instruction right after the trap, dropping back to user mode.
# RISC-V (Linux): write "Hi\n" to the screen, then exit cleanly. # A system call = put a number + args in registers, then 'ecall'. li a7, 64 # a7 = syscall number 64 -> 'write' li a0, 1 # a0 = file descriptor 1 -> standard output la a1, msg # a1 = address of the bytes to write li a2, 3 # a2 = how many bytes ecall # trap into the kernel; it does the write, returns li a7, 93 # a7 = syscall number 93 -> 'exit' li a0, 0 # a0 = exit status 0 -> success ecall # trap again; the kernel ends this process
Stepping back: read it, don't write it
Pull this rung together. Across these five guides you watched a high-level program melt all the way down to the metal: expressions flattened into register arithmetic, ifs and loops into conditional branches, the stack growing and shrinking under procedure calls and recursion, a calling convention letting strangers' code cooperate, and now the linker, loader, and system call connecting your code to the libraries beside it and the operating system above it. Each step removed one layer of comfortable fiction until nothing was left but instructions, registers, and memory. That descent is the whole point: the compiler and the OS together hide a staggering amount of machinery so you can think in C.
One honest closing note, the same one this rung opened with. In 2026 almost nobody hand-writes assembly for real work — compilers allocate registers and schedule instructions better than people can, and they never tire. What stays priceless is the ability to read it. Reading the assembly a compiler produced is how you learn why one version of a loop runs many times slower than another with identical results, how you debug a crash dump with no source in sight, and how you check that a security-critical routine really does what it claims. You do not need to become an assembly programmer. You do need to never again be mystified by what the machine is actually doing beneath your code.