COMP 3150Operating Systems

01 Introduction

Lesson 8.2 introduced address binding and the distinction between logical and physical addresses. It presented the three binding times and showed how a modern system layers them: compile-time binding for the internal layout of a module, load-time binding for resolving imports, and execution-time binding for the physical mapping. This lesson turns to a different problem: which parts of a program need to be in memory at all?

A program is not a homogeneous blob of code and data. It is a collection of routines — functions, methods, procedures — most of which are never called during any given run. A word processor contains a spell-checker, a print engine, an import filter for a dozen file formats, and a macro language interpreter; a typical session uses only a small fraction of them. A web browser contains support for dozens of network protocols, media codecs, and scripting languages; a page that does not play video does not need the video decoder. The question is whether the operating system should load all of a program into memory when the program starts, or only the parts that are actually used.

Dynamic loading is the answer: load a routine only when it is first called. The program is stored on disk in a relocatable form, and the main program begins execution with only a small portion in memory. When a routine that is not yet in memory is called, a loader — provided as part of the program's own runtime, not by the operating system — locates the routine's object code on disk, loads it into memory, relocates it if necessary, and transfers control to it. The routine stays in memory for the remainder of the program's execution, so subsequent calls to it are direct.

The advantage of dynamic loading is that a routine that is never called is never loaded, and a routine that is called only occasionally is loaded only when needed. The total amount of memory used by the program is therefore determined by the routines it actually executes, not by the routines it contains. For a program like a word processor or a web browser, this can reduce the memory footprint by a factor of ten or more. The corresponding disadvantage is the overhead of loading a routine at the moment it is needed: the first call to a dynamically loaded routine is much slower than a direct call, because it must read the routine from disk and relocate it. On a system with good locality, however, the routines are loaded once at the beginning of the program and the loading cost is amortised over the whole run.

This lesson presents dynamic loading and its relationship to the other mechanisms in the unit. It distinguishes dynamic loading from static loading and from dynamic linking — three concepts that are often confused — and it presents the historical precursor, overlays, which achieved a similar effect in an era before virtual memory. It introduces swapping, the first mechanism for handling the case where the total memory demanded by the system's processes exceeds the physical memory — the overcommitment requirement from Lesson 8.1. And it closes with the observation that the ideas of dynamic loading are the direct ancestors of the demand paging of Unit 9, which generalises them from routines to pages.

Prerequisites

This lesson builds directly on Lesson 8.2. The three binding times, the distinction between logical and physical addresses, the memory-management unit, and the base-and-limit scheme are all assumed. The distinction between static and dynamic linking from section 4.6 of Lesson 8.2 is central to this lesson, and the discussion of position-independent code and the Global Offset Table will be extended here. No prior knowledge of overlays or swapping is assumed; both are introduced from first principles.

02 Learning objectives

By the end of this lesson you will be able to

  • Distinguish static loading from dynamic loading and explain the trade-offs of each.
  • Describe how dynamic loading works, including the role of the loader and the sticky-residency rule.
  • Distinguish dynamic loading from dynamic linking and explain how the two can be combined.
  • Describe overlays and explain why they were used before virtual memory.
  • Explain swapping and the relationship between dynamic loading, swapping, and demand paging.

This lesson contributes to U8.2 and U8.3, and to course learning outcomes CLO2, CLO4, and CLO6.

03 Required reading

  • Operating System Concepts — Chapter 9, Section 9.1, Background, including the discussion of dynamic loading and dynamic linking. The standard treatment of both topics.
  • Tanenbaum & Bos — Modern Operating Systems, Chapter 3, Section 3.3, Virtual Memory, and Section 3.4, Page Replacement Algorithms (the introductory part). Read for the historical context and for the discussion of overlays.
  • OSTEP — Chapter 18, Paging: Introduction, and Chapter 21, Swapping: Mechanisms. The clearest available treatment of how the ideas of dynamic loading generalise into demand paging.
  • Bryant, R. E., & O'Hallaron, D. R. — Computer Systems: A Programmer's Perspective, 3rd ed., Chapter 7, Linking, sections 7.9–7.13, on shared libraries and runtime loading. The book-length treatment of the toolchain that performs dynamic linking.
  • Levine, J. R. — Linkers and Loaders. Morgan Kaufmann, 1999. Chapters 1 and 2, and Chapter 8 on dynamic linking. The definitive reference for the mechanics.
  • Linux man pages: man 3 dlopen, man 3 dlsym, man 3 dlerror, man 3 dlclose, man 1 ld.so, man 1 ldd, man 2 mprotect, man 5 proc (the /proc/<pid>/maps interface).

Reading strategy

Read the Silberschatz section first for the formal definitions and the distinction between dynamic loading and dynamic linking. Then read the Bryant & O'Hallaron sections on shared libraries for the toolchain-level view, and the OSTEP chapter on swapping for the modern continuation of the idea. The three treatments are complementary: Silberschatz gives the conceptual framework, Bryant & O'Hallaron gives the concrete implementation on real systems, and OSTEP shows where the ideas lead. Pay particular attention to the distinction between dynamic loading and dynamic linking — they are frequently confused, and the confusion obscures why both exist.

04 Core content

4.1 Static loading

Static loading is the default behaviour of a traditional loader. When a program is started, the operating system reads the entire executable image into memory — all its code, all its data, all its statically linked libraries — and only then transfers control to the program's entry point. The program's memory footprint is fixed at load time and does not change throughout its execution (except through explicit requests for more memory, such as malloc and mmap).

Static loading has three properties that made it the natural default in the early days of computing and that it still has today.

  • Simplicity. The loader reads the program into memory, sets up the address space, and jumps to the entry point. There is no runtime machinery for loading code, no need to keep track of which routines are resident, and no need to handle the case where a routine must be brought in from disk in the middle of execution. The loader is a small, simple program.
  • Predictability. The program's memory footprint is known in advance. The operating system can decide whether the machine has enough memory to run the program before starting it; if it does not, the program is not started. The program's performance is uniform throughout its execution, because there are no loading pauses.
  • Direct execution. Once the program is loaded, every instruction fetch and every data access reaches memory directly. There is no indirection, no check for residency, no trampoline through a loader. The program runs at full speed.

The disadvantage of static loading is that the memory footprint includes everything the program might use, whether or not it is used in a given run. A word processor that loads its full feature set — text editor, spell-checker, grammar-checker, print engine, import filters, export filters, macro interpreter, collaboration client, cloud-sync engine — uses memory for all of them, even if the user opens a file, edits it, and saves it without using any of the features except the editor. On a system with limited memory, this can be the difference between the program fitting and not fitting.

4.2 Dynamic loading

Dynamic loading is the alternative: load a routine into memory only when it is first called. The program is stored on disk in a form that allows individual routines to be loaded independently — typically a relocatable object file per routine, or a single module with a table of routine locations. When the program begins, only the main routine and its immediate dependencies are loaded. When a routine is called for the first time, a check determines whether it is already in memory. If it is, the call proceeds directly. If it is not, the dynamic loader is invoked to bring the routine in from disk, relocate it, and transfer control to it.

The check and the loader are part of the program's own runtime, not of the operating system. This is the crucial point, and it is the source of much confusion. In the classical formulation presented in textbooks, the dynamic loader is a routine that the programmer links into the program, and the program itself is responsible for deciding when to call it. The operating system provides only the primitive mechanisms — reading a file, allocating memory, mapping memory — that the dynamic loader uses.

The algorithm of dynamic loading is direct. Every call to a potentially-unloaded routine is transformed into a call to a stub, which performs the following steps:

  1. Check whether the target routine is already resident in memory.
  2. If it is resident, jump to its entry point. (On subsequent calls, this is the only step.)
  3. If it is not resident, locate its object code on disk.
  4. Read the object code into a free region of memory.
  5. Relocate the code — adjust any addresses in it to reflect its new load address. This is the same process that the loader performs for a program at startup, but performed for a single routine at runtime.
  6. Mark the routine as resident, so that subsequent calls will take the fast path in step 2.
  7. Transfer control to the routine's entry point.

The key observation is that the routine, once loaded, stays in memory for the rest of the program's execution. It is not evicted when the call returns; there is no replacement policy. This "sticky residency" rule is what makes dynamic loading different from the demand paging of Unit 9, where pages can be evicted and reloaded. In dynamic loading, a routine is either not yet loaded (its first call has not happened) or is resident (its first call has happened and it will never be evicted). The memory usage of the program is therefore monotonically increasing: each newly-loaded routine consumes memory that will not be reclaimed until the program terminates.

The programmer's role in dynamic loading

In the classical formulation, dynamic loading is something the programmer arranges explicitly. The programmer decides which routines should be dynamically loaded and replaces the calls to those routines with calls to stubs that invoke the loader. The programmer therefore has direct control over the trade-off: load everything at startup (fast, uses more memory) or load routines on demand (slower first call, uses less memory). Modern systems automate much of this through dynamic linking and demand paging, but the underlying idea — load only what is used — remains the same.

Static loading versus dynamic loading A diagram comparing static and dynamic loading. On the left, static loading: the entire program image, including all its routines, is loaded into memory at startup. On the right, dynamic loading: only the main routine is loaded at startup; routines are loaded into memory on demand, each on its first call, and each stays in memory thereafter. The dynamic version uses less memory when routines are never called, and the loading cost is paid only for routines that are actually used. STATIC LOADING VERSUS DYNAMIC LOADING Static loading Memory at program start main() + its dependencies spell_checker() grammar_checker() print_engine() import_filter() macro_interpreter() every routine uses memory, whether called or not Dynamic loading Memory at program start main() only the entry point is loaded spell_checker() — not yet loaded grammar_checker() — not yet loaded print_engine() — not yet loaded import_filter() — loaded on first call macro_interpreter() — not yet loaded only routines that are actually called use memory Disk image contains all routines Dynamic loader brings in routines on demand Dynamic loading trades the cost of loading routines that are never called for the cost of a first-call delay on routines that are.
Figure 8.3 — Static loading versus dynamic loading. On the left, all routines of the program are loaded into memory at startup. On the right, only the main routine is loaded at startup, and each additional routine is loaded on its first call and stays in memory thereafter. The dynamic approach uses less memory when routines are never called, at the cost of a first-call delay for the routines that are.

4.3 What dynamic loading costs

The trade-off of dynamic loading is between memory and time. The memory saving comes from not loading routines that are never used; the time cost comes from the delay on the first call to each routine that is used. The balance depends on the workload.

For a workload that uses all of a program's routines, the memory saving is zero and the time cost is positive: dynamic loading is strictly worse than static loading. For a workload that uses only a small fraction of the routines, the memory saving is substantial and the time cost is paid only for the routines that are used. For a program that is interactive and starts many routines in quick succession, the time cost may be visible to the user as a stutter at the moment of the first call. For a program that starts once and runs for hours, the time cost is negligible and the memory saving is significant.

There is also a subtle cost in complexity. The program must include the dynamic loader, must keep track of which routines are resident, and must structure the calls to potentially-unloaded routines through stubs. In a modern system, this complexity is largely handled by the toolchain — the linker and the dynamic linker generate the stubs and maintain the state — but in the classical formulation the programmer wrote them. Either way, the mechanism adds a layer of indirection that must be maintained and debugged.

Dynamic loading is not the same as demand paging

Both load code on demand, but they differ in their granularity and in their replacement policy. Dynamic loading loads a routine — a logical unit of code the programmer has chosen — and never evicts it once loaded. Demand paging loads a page — a fixed-size block of memory the hardware has chosen — and may evict it later if memory becomes scarce. Dynamic loading is a programmer's technique; demand paging is an operating system mechanism. The two can be combined, and in a modern system they are: a dynamically linked library is loaded by the dynamic linker and its pages are then demand-paged by the kernel like any other memory.

4.4 Dynamic linking revisited

Lesson 8.2 introduced dynamic linking as the toolchain's choice between copying library code into an executable (static linking) or leaving the resolution of library symbols until load time or first use (dynamic linking). It is worth revisiting the distinction between dynamic linking and dynamic loading, because the two are frequently confused and the confusion obscures why both exist.

Dynamic loading versus dynamic linking
Aspect Dynamic loading Dynamic linking
Unit of loading A routine (a function or procedure) A library (a collection of routines and data)
Who decides when? The programmer, explicitly — often at the point of first call The toolchain and the dynamic linker, implicitly — at load time or first use
Granularity Fine — one routine at a time Coarse — an entire library at a time
Main purpose Save memory by not loading unused routines Share library code among processes; allow library updates without recompilation
Residency policy Sticky — a loaded routine stays in memory until the program ends Depends on the OS — the library's pages are demand-paged and may be evicted
Implemented by The program's own runtime, using the OS's file and memory primitives The dynamic linker (ld.so) in cooperation with the kernel
Classical example An editor that loads its print engine only when the user prints A program linked against libc.so.6, which is shared by every process on the system

The two mechanisms are complementary, not competing. Dynamic linking is what allows a library to be shared among processes: without it, every program would have its own copy of every library it uses, and the total memory consumed by libraries on the system would be the sum of the sizes of the libraries each program links. Dynamic loading is what allows a single program's own routines to be loaded on demand: without it, every routine that the program contains would occupy memory whether or not it is ever called.

In a modern system, the two are combined. A program is dynamically linked against its libraries, which means the libraries are shared. And the program's own routines — particularly if it is a large program — may be laid out in a form that allows the operating system's demand paging to load them only as they are used. The distinction between dynamic loading and demand paging blurs at this point: the programmer no longer writes stubs, because the operating system's page fault mechanism does the loading automatically. But the underlying idea — load only what is used — is exactly the idea of dynamic loading.

4.5 Overlays: the historical precursor

Before virtual memory was practical, the problem of running a program larger than physical memory had to be solved by the programmer. The technique was called overlays, and it was the dominant approach from the late 1950s through the 1970s.

An overlay is a portion of a program that shares memory with another portion. The programmer divides the program into overlay segments: a resident portion that is always in memory, and a series of transient portions that occupy the same memory region at different times. The programmer is responsible for knowing which segments can coexist and which must be swapped. When the program reaches the point where segment A must be replaced by segment B, the program itself issues the request to load B into the shared region, overwriting A.

The overlay approach is essentially manual dynamic loading with an additional twist: the program's segments can replace each other, rather than accumulating. It requires the programmer to construct a call graph — a diagram of which routines call which other routines — and to partition the routines into segments such that routines that never call each other can share the same memory. The partition is a graph-colouring problem, and it is NP-hard in general, so the programmer uses heuristics.

Overlays are instructive because they show what the alternative to virtual memory looks like. Without hardware and operating-system support for demand paging, the programmer must solve the loading problem by hand, using knowledge of the program's structure that the operating system does not have. The result is labor-intensive and error-prone: a mistake in the overlay partition causes a routine to be overwritten while it is still in use, and the resulting bug is difficult to diagnose. When virtual memory became practical in the 1970s and 1980s, overlays disappeared, because the operating system could solve the same problem automatically using the page fault mechanism.

Overlays in modern systems

The word "overlay" survives in a different sense in modern systems. An overlay file system (such as Docker's storage driver or overlayfs in Linux) combines multiple file system layers into a single view, and the word is used because the layers "overlay" each other. This is not the same concept as program overlays, but the two share the idea of one thing being stacked on top of another. The classical overlay technique — the manual partitioning of a program into mutually-exclusive segments — is essentially extinct.

4.6 Swapping

Dynamic loading and overlays solve the problem of a single program that is too large for memory. They do not solve the problem of many programs that together are too large for memory. That problem is solved by swapping.

In a swapping system, the operating system moves an entire process's address space — all of its code, data, heap, and stack — between main memory and a region of disk called the swap space (or backing store). When the system needs to free memory for a new process, it swaps out a process that is not currently runnable — a process that is waiting on I/O, for example, or a process that has been preempted for a long time — writing the entire process to disk. When the process is needed again, the system swaps it in, reading it back into memory.

Swapping has several consequences.

  • It requires a large amount of disk space. The swap area must be at least as large as the total memory that might be swapped out at once, plus a margin. Systems with tens of gigabytes of physical memory typically allocate tens of gigabytes of swap space.
  • It is slow. Writing a process to disk and reading it back is expensive — milliseconds of latency for a large process — and it happens on a time scale that is visible to the user. A system that is swapping heavily is perceived as sluggish or frozen.
  • It requires a policy for choosing victims. Which process should be swapped out when memory is needed? The classical policies are based on priority (swap out the lowest-priority process), on the process's recent CPU usage (swap out the process that has used the least CPU recently), and on the process's memory footprint (swap out the largest process, which frees the most memory). The choice matters, because a bad victim-selection policy can lead to thrashing: the system spends all its time swapping processes in and out and no time doing useful work.
  • It is coarse-grained. Swapping moves an entire process's address space. If a process has a large address space but is only using a small part of it, the whole thing is moved. The fine-grained alternative — moving individual pages instead of whole processes — is what demand paging provides, and it is what makes modern systems efficient.

Swapping is the direct ancestor of demand paging. The idea is the same: move data between main memory and disk to handle overcommitment. The difference is the granularity. Swapping moves a whole process; demand paging moves a single page. Demand paging is more efficient because it can make better use of partial residency: a process whose working set is small can be fully resident even if its total address space is large. Unit 9 is about demand paging.

Many modern systems do not implement classical swapping, but they do have a swap area, and they do use it for demand paging. The swapon and swapoff commands on Linux control the swap areas, and /proc/swaps lists them. When demand paging evicts a page, it writes the page to swap; when the page is needed again, it reads the page back. The swap area is therefore the backing store for demand paging, and it plays the same role that the swap area played for classical swapping — but at page granularity.

Swapping and its relationship to demand paging A diagram showing main memory on the left and disk on the right, with a swap area on the disk. On the left, several processes are shown in main memory, and one is being swapped out. On the right, the swap area holds the swapped-out process. A curved arrow shows the swap-out operation (moving the entire process to disk) and the swap-in operation (bringing it back). Below, a comparison shows that swapping moves whole processes while demand paging moves individual pages. SWAPPING AND ITS RELATIONSHIP TO DEMAND PAGING Main memory Process A (running) Process B (blocked on I/O) Free — space for a new process Process C (low priority) The OS needs space for a new process. Disk Swap area holds processes (or pages) that have been evicted Free disk space Files, libraries, etc. swap out swap in Swapping versus demand paging Swapping: moves an entire process address space between memory and disk. Granularity: one process. Coarse-grained. Requires a policy to choose victims. Used in early time-sharing systems; mostly superseded by demand paging. Demand paging: moves individual pages between memory and disk. Granularity: one page. Fine-grained. Uses a page-replacement policy.
Figure 8.3b — Swapping and its relationship to demand paging. Swapping moves an entire process address space between main memory and the swap area on disk. Demand paging, the subject of Unit 9, moves individual pages. Both handle the overcommitment requirement from Lesson 8.1, and both use a swap area as backing store. The difference is granularity: swapping moves a whole process, demand paging moves a single page.

4.7 Modern practice: dlopen and friends

The classical dynamic loader — a routine linked into the program and called explicitly by the programmer — survives in modern systems as the dynamic loading API, provided by the POSIX standard and implemented by the dynamic linker. The API is a small set of functions in <dlfcn.h>:

dlopen
Loads a shared library at runtime and returns a handle to it. The library may be specified by name or by path. The call returns NULL if the library cannot be loaded. The library is mapped into the process's address space, and its dependencies are recursively loaded as well.
dlsym
Looks up a symbol (function or variable) in a loaded library, using the handle returned by dlopen. The call returns the symbol's address, which the caller can cast to the appropriate function pointer type and then call.
dlclose
Unloads a library that was loaded with dlopen. The library's memory is released when its reference count reaches zero. In practice, many libraries are never unloaded, because the reference counting and the possibility of dangling pointers make unloading dangerous.
dlerror
Returns a human-readable string describing the most recent error from dlopen, dlsym, or dlclose. The error string should be retrieved immediately after a failed call, because subsequent calls may overwrite it.

The API is used in several common situations:

  • Plugins. A program that supports user-installable plugins — an image editor, a web browser, a build tool — loads the plugin's shared library at runtime when the plugin is invoked. The plugin provides a well-known symbol (its entry point), which the host program looks up with dlsym and calls.
  • Optional features. A program that has features that are useful only in some contexts — hardware acceleration, support for a particular file format, a licensing module — can load the feature's library only when the feature is requested. The program's initial memory footprint is smaller, and the code for unused features is not loaded at all.
  • Language runtimes. A language runtime that supports foreign-function interfaces — Python's ctypes, Java's JNI, Node.js's node-gyp — loads a native library at runtime, looks up the native functions, and calls them from the interpreted language.
  • Dynamic configuration. A program can choose among several implementations of a feature at runtime — for example, choosing the appropriate device driver for the hardware it detects — by loading the corresponding library.

The dlopen API is the modern form of dynamic loading. It loads at library granularity rather than at routine granularity, and it is explicit in the code rather than generated by the toolchain. But the underlying idea — load what you need when you need it — is the same idea that the classical dynamic loader implemented.

4.8 The relationship to virtual memory

Dynamic loading, dynamic linking, and swapping are all solutions to the same underlying problem: the total code and data of the system's programs exceeds the physical memory available to hold it. The problem is overcommitment, introduced in Lesson 8.1 as one of the three requirements that makes memory management difficult.

Each mechanism solves the problem at a different granularity and with different assumptions:

  • Overlays solve it at the granularity of programmer-chosen segments, with the programmer responsible for the partition. It requires detailed knowledge of the program's call structure and works only for a single program.
  • Dynamic loading solves it at the granularity of routines, with the programmer responsible for identifying which routines should be loaded on demand. It requires per-routine stubs and a runtime loader, and it works for a single program's routines.
  • Dynamic linking solves it at the granularity of libraries, with the toolchain and the dynamic linker responsible for the mechanism. It requires position-independent code and a dynamic linker, and it shares library code among all processes on the system.
  • Swapping solves it at the granularity of entire processes, with the operating system responsible for the policy. It requires a swap area and a victim-selection policy, and it handles the case where the sum of all processes' addresses spaces exceeds physical memory.
  • Demand paging — the subject of Unit 9 — solves it at the granularity of pages, with the operating system responsible for the mechanism and the policy. It requires a page table, a translation lookaside buffer, and a page-replacement policy, and it subsumes the granularities of all the mechanisms above: a page can be part of a routine that is dynamically loaded, part of a library that is dynamically linked, or part of a process that would otherwise be swapped.

The trend is clear: the trend is toward finer granularity and toward moving the responsibility from the programmer to the operating system. Overlays require the programmer to partition the program by hand; dynamic loading requires the programmer to write stubs; dynamic linking moves the mechanism to the toolchain; swapping and demand paging move it to the kernel. Each step makes the mechanism more automatic and more general, at the cost of more complex hardware and operating-system support.

Demand paging is the endpoint of this progression: it handles overcommitment at the finest possible granularity, with no programmer involvement at all, and it generalises all the mechanisms above it. The rest of Unit 8 presents the classical allocation techniques that were used before paging was universal, and Unit 9 presents demand paging itself. The classical techniques — contiguous allocation, fragmentation, segmentation — are not obsolete; they are the foundation on which paging was built, and understanding them is what makes the design of paging legible.

4.9 Common misconceptions

"Dynamic loading and dynamic linking are the same."

They are different mechanisms for different problems. Dynamic loading loads individual routines on demand; dynamic linking shares entire libraries among processes. The two are often used together, but they solve different problems.

"Dynamic loading evicts routines when memory is scarce."

Classical dynamic loading has sticky residency: a loaded routine stays in memory until the program terminates. Eviction is the job of demand paging, not of dynamic loading.

"The operating system implements dynamic loading."

In the classical formulation, the dynamic loader is part of the program, not of the operating system. The OS provides the primitive operations (read a file, allocate memory, map memory), and the program uses them. Modern systems blur the line through dlopen and the dynamic linker, but the underlying mechanism is still user-space code cooperating with the kernel.

"Overlays are obsolete, so they are not worth studying."

Overlays are the direct precursor of demand paging, and understanding them makes the design of demand paging legible. They also illustrate the general principle of solving overcommitment by partitioning the working set, which is what every modern memory management technique does.

"Swapping is the same as demand paging."

Swapping moves an entire process address space; demand paging moves individual pages. Swapping is the coarser mechanism, and it is mostly superseded by demand paging in modern systems, but the swap area it introduced is still used as the backing store for paged-out pages.

"Dynamic loading slows a program down because of the loading."

The loading cost is paid only on the first call to a routine, and it is amortised over all subsequent calls. For a program that runs for a long time, the cost is negligible. For a short-lived program that uses many routines, the cost may be visible.

"dlopen is for libraries, not for programs."

dlopen loads a shared object, which may be a library or a program's own dynamically loaded component. The distinction between "library" and "program" is a matter of convention; the file format and the loading mechanism are the same.

"Since memory is cheap, dynamic loading is unnecessary."

Memory is cheaper than it was, but programs have grown correspondingly. A modern web browser uses several gigabytes of memory, and the fraction of its code that is loaded is still only a small part of what it could theoretically use. Dynamic loading remains relevant whenever a program's code exceeds its actual working set.

05 Worked example

The following example implements a simple plugin system with dlopen and dlsym, and compares its behaviour to a statically linked version.

A plugin system with dlopen

The scenario is a calculator program that supports operations loaded from shared libraries at runtime. The calculator itself knows only how to read an operation name and two operands; it discovers the operation's implementation by loading the corresponding shared library.

First, the plugin interface. A header file defines the symbol that every plugin must export:

/* plugin.h — the interface shared by the calculator and its plugins */
#ifndef PLUGIN_H
#define PLUGIN_H

typedef int (*binary_op_t)(int a, int b);

/* Every plugin exports a symbol named "op" of this type. */
extern binary_op_t op;

#endif

Next, two plugins. Each is compiled into a shared library.

/* add.c — plugin for addition */
#include "plugin.h"

static int add(int a, int b) { return a + b; }
binary_op_t op = add;
/* mul.c — plugin for multiplication */
#include "plugin.h"

static int mul(int a, int b) { return a * b; }
binary_op_t op = mul;

Finally, the calculator. It takes three arguments — the operation name, and the two operands — loads the corresponding plugin, looks up the op symbol, and calls it.

/* calc.c — the host program */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "plugin.h"

int main(int argc, char **argv) {
    if (argc != 4) {
        fprintf(stderr, "usage: %s OP A B\n", argv[0]);
        return 1;
    }

    const char *opname = argv[1];
    int a = atoi(argv[2]);
    int b = atoi(argv[3]);

    /* build the library filename: "libOP.so" */
    char libname[256];
    snprintf(libname, sizeof(libname), "./lib%s.so", opname);

    void *handle = dlopen(libname, RTLD_NOW);
    if (!handle) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        return 1;
    }

    /* clear any previous error */
    dlerror();

    binary_op_t *op_ptr = dlsym(handle, "op");
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym: %s\n", err);
        dlclose(handle);
        return 1;
    }

    printf("%s(%d, %d) = %d\n", opname, a, b, (*op_ptr)(a, b));

    dlclose(handle);
    return 0;
}

Compile the plugins and the host:

$ gcc -shared -fPIC -o libadd.so add.c
$ gcc -shared -fPIC -o libmul.so mul.c
$ gcc -o calc calc.c -ldl

$ ./calc add 3 4
add(3, 4) = 7

$ ./calc mul 3 4
mul(3, 4) = 12

$ ./calc div 3 4
dlopen: ./libdiv.so: cannot open shared object file:
        No such file or directory

The calculator loads only the library for the operation it is asked to perform. If the user asks for addition, the multiplication library is never opened. If the user asks for an operation for which no plugin exists, the calculator reports the error and exits cleanly.

What the program does not know at compile time

The calculator's source code does not mention add or mul. It contains no reference to the plugins' functions. The calculator's binary contains no code from the plugins and no absolute address of the plugins' functions. The binding of the operation to its implementation happens entirely at runtime: the user's argument names the library, dlopen loads it, and dlsym finds the symbol.

This is the essential property of dynamic loading. The program's behaviour can be extended without recompiling or even modifying the program. New plugins can be added by the user, and the calculator will use them without any change. The same technique is used by image editors, audio players, web browsers, and language runtimes.

Comparing to the classical dynamic loader

The dlopen API loads a shared library at library granularity. The classical dynamic loader described in the textbook loads an individual routine at routine granularity. The difference matters for the mechanism but not for the principle:

Aspect Classical loader dlopen
Unit of loading One routine One shared library (containing many routines)
When it loads First call to the routine Explicit call to dlopen
Who decides The stub, transparently to the caller The program, explicitly
Residency policy Sticky — the routine is never evicted Reference-counted; dlclose may unload
Symbol resolution By the loader, using the routine's name By the caller, using dlsym

The modern API has two advantages over the classical loader. First, it works with the standard dynamic linker, so the loaded library can depend on other libraries and on the program's own symbols. Second, it is explicit in the source code, so the programmer can see exactly which parts of the program are dynamically loaded and decide the trade-off accordingly.

Memory and startup cost

Consider the memory savings in a realistic case. A typical image editor has, in addition to its core code, about thirty import/export plugins for different file formats. Each plugin is around 100 KB. If the editor loaded all plugins at startup, its memory footprint would be about 3 MB larger than if it loaded only the plugin for the format the user opens. For a user who edits one file and closes the program, the savings are the entire 3 MB (minus the one plugin that is loaded); for a user who opens files in every format, the savings are smaller but still positive.

The startup cost is the time to load one plugin when the user requests the format — typically a few milliseconds, since the plugin is small and the disk cache usually holds it after the first use. The savings in startup memory, by contrast, is spread over every launch of the program. For a program that is launched frequently, the accumulated savings are substantial.

The cost of the indirection

The plugin interface introduces a level of indirection. The calculator cannot call add directly, because it does not know that add exists. It must call through a pointer obtained from dlsym. The overhead of the indirection is a single load of the pointer, which is negligible compared to the work the operation itself performs.

In a system where a dynamically loaded routine is called in a tight loop, the indirection may become visible. In that case, the program should obtain the pointer once and cache it, rather than calling dlsym on every call. The dlsym function itself performs a symbol table lookup, which is more expensive than a single pointer indirection, and it should not be called from inside a hot loop.

06 Interactive activity

Activity · implement a plugin system with dlopen

The goal of this activity is to build a small plugin system, observe the dynamic loading in action, and compare its memory footprint to a statically linked alternative. Work through the steps in your course VM.

  1. Build the plugin system. Implement the calculator and its two plugins from the worked example. Compile the plugins as shared libraries and the calculator against libdl.
    gcc -shared -fPIC -o libadd.so add.c
    gcc -shared -fPIC -o libmul.so mul.c
    gcc -o calc calc.c -ldl
    
    ./calc add 3 4
    ./calc mul 3 4
    Verify that both plugins work correctly.
  2. Confirm that the calculator does not contain the plugins. Use nm or objdump to look at the calculator's symbols, and confirm that neither add nor mul appears. Then use ldd to list the calculator's shared library dependencies, and confirm that neither plugin is in the list.
    nm calc | grep -E '\b(add|mul)\b'
    ldd calc
    The calculator's binary is unaware of the plugins at link time.
  3. Trace the dynamic loading. Use strace to see the system calls the calculator makes when it loads a plugin. Look for the openat and mmap calls that load the plugin's file and map it into memory.
    strace -f -e trace=openat,mmap,mmap2 ./calc add 3 4 2>&1 | \
        grep -E 'libadd|libmul' | head -20
    Note that the plugin file is opened only when the operation requests it, not at program startup.
  4. Observe the address space. Write a version of the calculator that sleeps after loading the plugin, and inspect its address space.
    /* add after dlopen: */
    /* sleep(30); */
    
    ./calc add 3 4 &
    PID=$!
    cat /proc/$PID/maps | grep libadd
    kill $PID
    The plugin's code and data appear as regions in the calculator's address space, at addresses chosen by the dynamic linker. Confirm that the plugin is loaded at a different address than the calculator itself.
  5. Add a third plugin and observe the savings. Write a third plugin for subtraction. Without recompiling the calculator, confirm that the new plugin works.
    /* sub.c */
    #include "plugin.h"
    static int sub(int a, int b) { return a - b; }
    binary_op_t op = sub;
    
    gcc -shared -fPIC -o libsub.so sub.c
    ./calc sub 10 4
    This is the essential property of dynamic loading: the calculator's behaviour is extended without any change to the calculator itself.
  6. Compare to a statically linked version. Write a statically linked version of the calculator that contains all three operations in a single binary. Compile it and compare its size to the dynamically linked version plus the plugins.
    gcc -o calc_static calc_static.c
    ls -l calc calc_static libadd.so libmul.so libsub.so
    ldd calc_static
    The statically linked version is a single self-contained binary that contains all three operations. The dynamically linked version has a small main binary plus three separate plugin files. Which consumes more disk space? Which consumes more memory when only one operation is used?
  7. Measure the first-call cost. Modify the calculator to load a plugin and call its operation in a loop a million times, and measure the time. Compare it to a statically linked version that calls the same operation a million times. The difference is the cost of the dlsym lookup, which can be eliminated by caching the pointer.
    /* Loop version: cache the pointer */
    binary_op_t op_fn = *op_ptr;
    for (long i = 0; i < 1000000; i++)
        result += op_fn(3, 4);
    Measure the version that calls dlsym on every iteration and compare to the version that caches the pointer. How much does the symbol lookup cost?
  8. Use LD_DEBUG to trace loading. Run the calculator with LD_DEBUG=libs set and observe the dynamic linker's activity.
    LD_DEBUG=libs ./calc add 3 4 2>&1 | head -30
    The output shows the libraries the dynamic linker loads, the search paths it consults, and the symbols it resolves. The plugin's loading appears as an additional dlopen call, separate from the initial loading of the program's own dependencies.
  9. Read man 3 dlopen. Read the man page and identify the flags that dlopen accepts: RTLD_LAZY, RTLD_NOW, RTLD_GLOBAL, RTLD_LOCAL, RTLD_NODELETE, RTLD_NOLOAD. For each, write a sentence describing what it does and when it might be useful. Then modify the calculator to use RTLD_LAZY instead of RTLD_NOW and observe whether the behaviour changes.

Reflect: dynamic loading makes the program's behaviour extensible and its memory footprint proportional to what it uses. But it also makes the program's behaviour dependent on files that are not part of it — the plugins — and on the dynamic linker that loads them. A plugin that is missing, or that exports the wrong symbol, or that depends on a library that is not present, causes a failure at runtime that would have been a compile-time error in a statically linked program. Is this the right trade-off? What mechanisms would you add to a dynamic loading system to catch these failures earlier?

07 Knowledge check

Check your understanding

Question 1

Describe static loading and dynamic loading. What are the advantages and disadvantages of each? Under what circumstances is each the right choice?

Show answer
Static loading reads the entire program image — all its code, all its data, all its statically linked libraries — into memory at startup, and the memory footprint is fixed at load time. Dynamic loading loads only the main routine at startup and brings each additional routine into memory on its first call, keeping it resident thereafter. The advantages of static loading are simplicity (the loader is small and has no runtime machinery), predictability (the memory footprint is known in advance), and direct execution (no indirection on any call). Its disadvantage is that the memory footprint includes everything the program might use, whether or not it is used in a given run. The advantages of dynamic loading are the converse: the memory footprint is proportional to what the program actually uses, so a program with many rarely-used routines has a much smaller footprint than it would under static loading. Its disadvantages are the first-call delay (the first call to a dynamically loaded routine must read the routine from disk and relocate it) and the complexity of the runtime loader. Static loading is the right choice when the program is small, when its memory footprint must be predictable (as in embedded systems with tight memory budgets), or when every routine is likely to be used in every run. Dynamic loading is the right choice when the program is large, when many of its routines are rarely called, or when the program must be extensible at runtime (as with a plugin system).

Question 2

Describe the algorithm of dynamic loading. What is the "sticky residency" rule, and how does it differ from the eviction policy of demand paging?

Show answer
The algorithm of dynamic loading transforms each call to a potentially-unloaded routine into a call to a stub, which performs the following steps: (1) check whether the target routine is resident; (2) if it is, jump to its entry point; (3) if it is not, locate its object code on disk; (4) read the code into a free region of memory; (5) relocate the code to reflect its new load address; (6) mark the routine as resident; (7) transfer control to the routine's entry point. The sticky residency rule is that once a routine has been loaded, it stays in memory for the remainder of the program's execution. There is no eviction: the routine is not removed when the call returns, and it is not removed when memory becomes scarce. The consequence is that a program's memory usage under dynamic loading is monotonically increasing: each newly-loaded routine consumes memory that will not be reclaimed until the program terminates. This differs fundamentally from demand paging, where a page that has been loaded may later be evicted to make room for another page. Demand paging has an eviction policy — typically some form of least-recently-used — because its goal is to keep the working set in memory while the total address space may exceed physical memory. Dynamic loading has no eviction policy because its goal is to defer the loading of routines that have not yet been called; once a routine has been called, it is assumed to be part of the working set and to remain useful. In a system that combines both — a modern system with dynamic linking and demand paging — the dynamically loaded library's pages are demand-paged like any other memory, so a page from a loaded library can be evicted if it is not recently used. The classical dynamic loader's sticky-residency rule is what demand paging generalises away.

Question 3

Distinguish dynamic loading from dynamic linking. How are the two mechanisms related, and how are they used together in a modern system?

Show answer
Dynamic loading loads an individual routine into memory on its first call, saving memory by not loading routines that are never used. Its unit is the routine, it is decided by the programmer, it is implemented by the program's own runtime, and its residency policy is sticky. Dynamic linking resolves a program's references to library symbols at load time or at first use, saving memory by sharing a single copy of a library among many processes. Its unit is the library, it is decided by the toolchain and the dynamic linker, it is implemented by the dynamic linker and the kernel, and its residency is demand-paged. The two mechanisms are complementary. Dynamic linking is what allows a library to be shared among processes: without it, every program would contain its own copy of every library it uses, and the total memory consumed by library code on the system would be the sum across all programs. Dynamic loading is what allows a single program's own routines to be loaded on demand: without it, every routine the program contains would occupy memory whether or not it is ever called. In a modern system, the two are used together. A program is dynamically linked against its libraries, so the libraries are shared; and the program's own code and the libraries' code are demand-paged by the kernel, so a page is loaded only when it is first accessed. The distinction between dynamic loading and demand paging blurs at this point, because the programmer no longer writes stubs — the kernel's page fault mechanism does the loading — but the underlying idea is exactly the idea of dynamic loading.

Question 4

What are overlays? Why were they used before virtual memory, and why did they disappear? What does the history of overlays tell us about the relationship between programmer responsibility and operating-system mechanisms?

Show answer
Overlays are a technique for running a program larger than physical memory, used from the late 1950s through the 1970s. The programmer divides the program into segments: a resident portion that is always in memory, and a series of transient portions that occupy the same memory region at different times. The programmer constructs a call graph of the program, partitions the routines into segments such that routines which never call each other can share memory, and arranges for one segment to overwrite another when the program reaches the transition between them. Overlays were used because there was no hardware or operating-system support for automatic loading at the required granularity. Memory was expensive and small, and programs routinely exceeded it. The programmer was the only agent with the knowledge needed to partition the program — the call graph, which routines call which — and so the programmer was given the responsibility. Overlays disappeared when virtual memory became practical in the 1970s and 1980s. Demand paging solved the same problem automatically: the operating system could page individual pages in and out based on which were actually being accessed, and the programmer no longer needed to partition the program by hand. The history of overlays illustrates a general pattern in operating-system design: a problem is first solved by the programmer, using detailed knowledge that the operating system does not have, and then as the operating system grows more sophisticated, the problem is solved automatically by the operating system, using mechanisms (page faults, replacement policies) that generalise the programmer's manual solution. The same pattern appears in many areas: the transition from programmer-managed overlays to OS-managed demand paging, from programmer-managed buffer pools to OS-managed page cache, from programmer-managed thread scheduling to OS-managed preemptive scheduling. In each case, the operating system's mechanism is more general and more reliable, at the cost of more complex hardware and more sophisticated kernel design.

Question 5

What is swapping? How does it differ from dynamic loading, and how does it differ from demand paging? What is the relationship between swapping, the swap area, and demand paging in a modern system?

Show answer
Swapping is the operating system's technique of moving an entire process address space — all of its code, data, heap, and stack — between main memory and a region of disk called the swap space (or backing store). When the system needs to free memory, it swaps out a process that is not currently runnable, writing the entire process to disk. When the process is needed again, it swaps it back in. Swapping differs from dynamic loading in its granularity and in who decides. Dynamic loading operates at the granularity of individual routines, and it is the programmer (or the toolchain) who decides which routines to load on demand. Swapping operates at the granularity of entire processes, and it is the operating system that decides which processes to swap out, using a victim-selection policy. Dynamic loading is a technique for a single program whose own routines exceed memory; swapping is a technique for a system whose processes together exceed memory. Swapping differs from demand paging in granularity as well. Swapping moves a whole process; demand paging moves a single page. Demand paging is more efficient because it can make better use of partial residency: a process whose working set is small can be fully resident even if its total address space is large, because only the pages it is currently using are in memory. This is why demand paging has largely superseded classical swapping. In a modern system, classical swapping is rarely implemented, but the swap area survives as the backing store for demand paging. When demand paging evicts a page, it writes the page to the swap area; when the page is needed again, it reads it back. The swap area therefore plays the same role it played for classical swapping — providing disk space for data that does not fit in memory — but at page granularity rather than process granularity. The swapon and swapoff commands on Linux control the swap areas, and /proc/swaps lists them. Even systems with large amounts of physical memory typically have a swap area, because demand paging uses it for any page that must be evicted, including anonymous memory from malloc and the stack, as well as the pages of a process that has been inactive for a long time.

08 Unit discussion

Discussion prompt

Dynamic loading and its relatives — dynamic linking, overlays, swapping, demand paging — form a progression in which the responsibility for a difficult problem moves from the programmer to the operating system. In overlays, the programmer partitions the program by hand, using knowledge of its call graph. In dynamic loading, the programmer identifies which routines to load on demand and writes the stubs. In dynamic linking, the toolchain and the dynamic linker take over the mechanism. In demand paging, the kernel does the work automatically, using the page fault as the signal and a replacement policy as the decision procedure.

This progression is not unique to memory management. It appears throughout operating-system history. In process scheduling, the programmer once had to yield the CPU explicitly (cooperative multitasking); now the kernel preempts (preemptive multitasking). In file I/O, the programmer once managed buffers by hand; now the kernel provides a page cache. In networking, the programmer once handled packets at the application level; now the kernel provides a socket abstraction. In each case, the operating system grew more capable, and the burden on the programmer decreased.

Is this progression always desirable? The automatic mechanisms are more reliable and more general, but they are also less transparent: a programmer debugging a performance problem in a demand-paged system has less visibility and less control than a programmer who partitioned their program with overlays. The automatic mechanism makes the common case fast and the uncommon case slow, and it gives the programmer fewer levers to pull when the common case does not apply.

Where do you think the balance should lie? Should the operating system continue to take over more responsibility, or should it expose more control to programs that need it? What kinds of problems are best served by automatic mechanisms, and what kinds are best served by giving the programmer explicit control? Post your response to the Unit 8 discussion forum and reply to at least two classmates. Supports CLO2, CLO4, and CLO6.

09 Practice exercise

This ungraded exercise consolidates the lesson and prepares you for Lesson 8.4. Aim to spend 60–90 minutes on it.

  1. Compare static and dynamic loading. Write a short paragraph (150–200 words) comparing static loading and dynamic loading. Your comparison should address memory usage, startup time, first-call cost, complexity, and predictability. Then state which form you would choose for (a) a small command-line utility that runs for a fraction of a second, (b) a large interactive application that runs for hours, and (c) an embedded system with a fixed task set. Justify each choice.
  2. Implement a plugin system. Build the calculator from the worked example with at least three plugins (add, multiply, and one of your own design). Confirm that the calculator works for each plugin without being recompiled. Then add a fourth plugin and confirm that the calculator uses it without any change.
  3. Measure the memory footprint. Compile a statically linked version of the calculator that contains all the operations inline, and compare its size to the dynamically linked version plus the plugins.
    ls -l calc calc_static lib*.so
    Which consumes more disk space? Which consumes more memory when only one operation is used? Which consumes more when all operations are used?
  4. Trace the loading with strace. Use strace to trace the system calls made by the calculator when it loads a plugin. Look for the openat, mmap, and mprotect calls that implement the loading. Write a paragraph describing the sequence of system calls and explaining what each one does.
    strace -e trace=openat,mmap,mprotect ./calc add 3 4 2>&1 | \
        grep -E 'libadd|libc' | head -20
  5. Compare to the classical loader. Write a small program that simulates the classical dynamic loader for a set of routines stored in separate files. The program should:
    • Maintain a table of routines and their locations on disk.
    • Maintain a table of resident routines.
    • When a routine is called, check the resident table.
    • If the routine is not resident, read it from disk into memory, mark it resident, and call it.
    struct routine {
        const char *name;
        const char *file;
        void       *addr;   /* NULL if not loaded */
    };
    
    struct routine routines[] = {
        { "add", "add.o", NULL },
        { "mul", "mul.o", NULL },
        { "sub", "sub.o", NULL },
    };
    
    void *load_routine(struct routine *r) {
        if (r->addr != NULL)
            return r->addr;       /* already loaded */
        /* read r->file into memory, relocate, store in r->addr */
        /* ... */
        return r->addr;
    }
    This simulation captures the essence of the classical dynamic loader: the check for residency, the loading on miss, and the sticky-residency rule.
  6. Analyse an overlay partition. Given the following call graph for a program — A calls B and C, B calls D, C calls D, D calls E — design an overlay partition that minimises the memory required. How many overlay segments do you need? Which routines can share memory? Draw the partition and explain your reasoning.
  7. Investigate the swap area on your system. On a Linux system, examine the swap areas with the following commands:
    swapon --show
    cat /proc/swaps
    free -h
    cat /proc/meminfo | grep -i swap
    How large is the swap area relative to the physical memory? Is the system currently using any swap? What does the answer tell you about the system's memory pressure?
  8. Reflect. In your own words, explain why dynamic loading is a trade-off between memory and time. Then describe one situation in your own experience — a program you have used or written — where dynamic loading would have been beneficial. What routines would you have loaded on demand, and what would the memory savings have been?

10 Summary

  • Static loading reads the entire program image into memory at startup. It is simple, predictable, and requires no runtime machinery, but its memory footprint includes every routine whether or not it is used.
  • Dynamic loading loads a routine into memory only when it is first called. It saves memory by not loading routines that are never used, at the cost of a first-call delay for the routines that are.
  • The dynamic loader is part of the program's runtime, not the operating system. The OS provides only the primitive operations — reading files, allocating memory, mapping memory — that the loader uses.
  • Sticky residency is the rule that a loaded routine stays in memory for the remainder of the program's execution. There is no eviction, and the program's memory usage under dynamic loading is monotonically increasing.
  • Dynamic loading and dynamic linking are different mechanisms for different problems. Dynamic loading loads individual routines on demand; dynamic linking shares entire libraries among processes. The two are complementary and are used together in modern systems.
  • Overlays were the historical precursor to dynamic loading. The programmer partitioned the program into segments and arranged for one to overwrite another. Overlays disappeared when demand paging made the same result automatic.
  • Swapping moves an entire process address space between memory and disk. It is coarser than demand paging, and classical swapping has largely been superseded by demand paging, but the swap area survives as the backing store for paged-out pages.
  • The dlopen API is the modern form of dynamic loading. It loads a shared library at runtime, and it is used for plugin systems, optional features, language runtimes, and dynamic configuration. The functions are dlopen, dlsym, dlclose, and dlerror.
  • Dynamic loading, dynamic linking, overlays, swapping, and demand paging are all solutions to the same underlying problem — overcommitment, introduced in Lesson 8.1 — at different granularities and with different assumptions. The trend is toward finer granularity and toward moving the responsibility from the programmer to the operating system.
  • Demand paging, the subject of Unit 9, is the endpoint of this progression. It handles overcommitment at the granularity of a page, with no programmer involvement, and it subsumes the granularities of all the mechanisms that came before it.

Looking ahead

This lesson presented dynamic loading and its relationship to the other mechanisms that handle overcommitment. Lesson 8.4 presents contiguous allocation: the classical approach to dividing physical memory among processes, in which each process occupies a single contiguous region. It introduces the allocation algorithms — first-fit, best-fit, worst-fit — and the free-list data structure that makes them possible. The lesson also introduces fragmentation in passing, which is the subject of Lesson 8.5, and it sets up the discussion of segmentation and paging that follows. Contiguous allocation is the simplest form of allocation, and its limitations are what motivate the more sophisticated mechanisms of the later lessons.

11 References

  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 9, Sections 9.1 and 9.2.
  • Bryant, R. E., & O'Hallaron, D. R. Computer Systems: A Programmer's Perspective, 3rd ed., Chapter 7, Linking, especially sections 7.9–7.13 on shared libraries and runtime loading.
  • Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 3, Sections 3.3–3.4.
  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapters 18 and 21. Available online at ostep.org.
  • Levine, J. R. Linkers and Loaders. Morgan Kaufmann, 1999. Chapters 1–2, and Chapter 8 on dynamic linking.
  • Hoare, C. A. R., & Wirth, N. An Axiomatic Definition of the Programming Language PASCAL. Acta Informatica, 1973. The original Pascal report, which mentions the overlay technique in its discussion of program structure.
  • Denning, P. J. Virtual Memory. ACM Computing Surveys, 1970. The paper that introduced the working-set model and framed the discussion of paging, which is the direct descendant of dynamic loading.
  • Linux man-pages: man 3 dlopen, man 3 dlsym, man 3 dlerror, man 3 dlclose, man 1 ld.so, man 1 ldd, man 8 swapon, man 5 proc.
👤