The Loader
An executable sitting on disk isn't running anything — the operating system's loader is what turns that file into an actual, live process in memory, ready for the CPU to begin executing.
The loader is the part of the operating system responsible for taking an executable file and turning it into a running process — allocating memory, copying (or mapping) the executable's code and data into that memory, loading any needed shared libraries, and finally handing control to the program's actual starting point.
A file on disk is just static bytes; a running program needs real memory, an execution context, and access to whatever shared libraries it depends on — the loader is precisely the step that bridges "a program exists as a file" to "a program is actually executing."
Every single time any program is launched — double-clicking an icon, running a command in a terminal, or the operating system starting one of its own background services — the loader runs as an essential, invisible step before the program's own code ever executes a single instruction.
Think of moving into a new apartment from a moving truck. The truck (the executable file on disk) holds all your furniture and belongings, but nothing is actually usable until movers (the loader) carry each labeled box into the correct room, set up the utilities (shared library dependencies), and hand you the keys, at which point you can finally start actually living there (the program starts executing).
By the time your program's main() function actually starts running, a substantial amount of OS and runtime-startup work has already silently happened on its behalf — memory is allocated, shared libraries are resolved and mapped in, and a runtime-provided startup routine has already set up the environment main() expects to find.
The entry point recorded in an executable's header (from the previous Binary & Executable Formats section) is typically not the programmer's own main() function directly — it's usually a small startup routine, provided by the language's runtime, that performs necessary setup (initializing the runtime, setting up argc/argv, running any needed static initializers) before finally calling the programmer's main().
Virtual memory mapping means the loader doesn't necessarily need to eagerly copy an entire executable file's contents into physical RAM upfront — modern operating systems can map an executable's segments into a process's virtual address space and load the actual underlying pages from disk lazily, on demand, only when a given part of the program is actually accessed (a technique called demand paging).
For a dynamically linked executable, the loader must also find, load, and map every needed shared library into the new process's address space, and resolve any remaining symbol references between the executable and those libraries — a substantial part of what's often called "dynamic linking," even though much of that work happens at load time, not at the earlier link (build) time step.
Before the program's own code runs, the loader (working with the runtime startup routine) also sets up the initial process stack, populates it with command-line arguments and environment variables, and establishes the heap in an initial, typically small state ready to grow as the program allocates memory dynamically.
Assuming main() is the very first code that runs in a program — in reality, a language runtime's own startup code almost always runs first, doing setup work the programmer never sees but that main() implicitly depends on.
Confusing the loader's job with the linker's — the linker (an earlier, build-time step) resolves and combines things ahead of time into one executable file; the loader (a later, run-time step, performed by the operating system, not the build toolchain) is what actually gets that finished file running as a live process, which for dynamically linked programs still involves further symbol resolution at load time.
Assuming an executable's full contents are always immediately and entirely read into physical memory the instant it starts — with virtual memory and demand paging, parts of a program that are never actually executed or accessed during a particular run may never be pulled into physical RAM at all.
Understanding "missing shared library" errors at program startup (rather than at compile or link time) as a load-time failure — the executable itself is fine, but the loader couldn't find or load a dynamically linked library it needs, often because that library isn't installed or isn't in a location the loader searches.
Recognizing that a program's very first few operations (however fast on a warm cache) can appear slower on a genuinely cold start, partly because demand paging is still pulling needed pages of the executable in from disk for the first time.
Appreciating why static linking (from the earlier Linker section) can result in more consistent, dependency-free startup — everything needed is already inside the one executable file, with no separate shared-library discovery step for the loader to perform.
The dynamic linker/loader as a distinct component On many systems, dynamic linking's load-time portion is handled by a distinct, specialized program often called the dynamic linker or interpreter (for example, ld.so/ld-linux.so on Linux) — the OS's initial loader actually loads and runs this dynamic linker first, which then takes over responsibility for finding and mapping in the executable's actual shared-library dependencies before finally transferring control to the program's real entry point.
Relocation at load time Because a program's exact load address (and a shared library's exact load address within it) may vary between runs, especially with address-space-layout-randomization security measures enabled, some addressing that couldn't be fully resolved even at link time must be finished by the loader itself at load time — applying load-time relocations is a genuinely distinct, later step from the relocations a linker applies at build time, even though both use the same underlying relocation-entry mechanism introduced back in the Assembler section.
Lazy binding (a common dynamic-linking optimization) Rather than resolving every single dynamically linked function reference eagerly at load time (which could noticeably slow down program startup for a program using many library functions it might not even call in a given run), many systems use lazy binding — a called function's real address is only resolved the very first time it's actually called, via a small stub (often called a PLT, or Procedure Linkage Table, entry) that performs the resolution on that first call and then redirects subsequent calls directly to the now-known real address.
What does the loader actually do?
It takes a compiled executable file and turns it into a running process — allocating and mapping memory for the program's segments, loading and mapping any needed shared libraries, setting up the initial stack and environment, and finally transferring control to the program's actual entry point.
Is main() typically the very first code that runs in a program?
No — a language runtime's own startup routine typically runs first (as the executable's actual recorded entry point), performing setup like initializing the runtime and preparing argc/argv, before it calls the programmer's main() function.
What's the difference between the linker and the loader?
The linker is a build-time tool that combines object files and resolves references ahead of time into one executable file. The loader is an operating-system component that runs later, at program-start time, turning that finished executable file into an actual running process in memory — for dynamically linked programs, it also performs additional symbol resolution for shared libraries at this later stage.
What is lazy binding, and why is it used?
A dynamic-linking optimization where a called library function's real address is only resolved the first time it's actually called, rather than eagerly resolving every possible dynamically linked reference at load time — reducing program startup time, especially for programs that link against large libraries but only actually call a subset of their functions in a given run.
Executable file (on disk)
|
v
OS loader: allocate virtual address space for a new process
|
v
Map the executable's segments into that address space (.text, .data, .bss, ...)
|
v
Load and map any needed shared libraries
|
v
Set up the initial stack, environment variables, and command-line arguments
|
v
Jump to the program's actual entry point (often not main() directly, but a small
runtime-startup routine that then calls main())