Hubbry Logo
Dynamic linkerDynamic linkerMain
Open search
Dynamic linker
Community hub
Dynamic linker
logo
7 pages, 0 posts
0 subscribers
Be the first to start a discussion here.
Be the first to start a discussion here.
Dynamic linker
Dynamic linker
from Wikipedia

In computing, a dynamic linker is the part of an operating system that loads and links the shared libraries needed by an executable when it is executed (at "run time"), by copying the content of libraries from persistent storage to RAM, filling jump tables and relocating pointers. The specific operating system and executable format determine how the dynamic linker functions and how it is implemented.

Linking is often referred to as a process that is performed when the executable is compiled, while a dynamic linker is a special part of an operating system that loads external shared libraries into a running process and then binds those shared libraries dynamically to the running process. This approach is also called dynamic linking or late linking.

Implementations

[edit]

Microsoft Windows

[edit]

Dynamic-link library, or DLL, is Microsoft's implementation of the shared library concept in the Microsoft Windows and OS/2 operating systems. These libraries usually have the file extension DLL, OCX (for libraries containing ActiveX controls), or DRV (for legacy system drivers). The file formats for DLLs are the same as for Windows EXE files – that is, Portable Executable (PE) for 32-bit and 64-bit Windows, and New Executable (NE) for 16-bit Windows. As with EXEs, DLLs can contain code, data, and resources, in any combination.

Data files with the same file format as a DLL, but with different file extensions and possibly containing only resource sections, can be called resource DLLs. Examples of such DLLs include multi-language user interface libraries with extension MUI, icon libraries, sometimes having the extension ICL, and font files, having the extensions FON and FOT.[1]

Unix-like systems using ELF, and Darwin-based systems

[edit]

In most Unix-like systems, most of the machine code that makes up the dynamic linker is actually an external executable that the operating system kernel loads and executes first in a process address space newly constructed as a result of calling exec or posix_spawn functions. At link time, the path of the dynamic linker that should be used is embedded into the executable image.

When an executable file is loaded, the operating system kernel reads the path of the dynamic linker from it and then attempts to load and execute this other executable binary; if that attempt fails because, for example, there is no file with that path, the attempt to execute the original executable fails. The dynamic linker then loads the initial executable image and all the dynamically-linked libraries on which it depends and starts the executable. As a result, the pathname of the dynamic linker is part of the operating system's application binary interface.

Systems using ELF

[edit]

In Unix-like systems that use ELF for executable images and dynamic libraries, such as Solaris, 64-bit versions of HP-UX, Linux, FreeBSD, NetBSD, OpenBSD, and DragonFly BSD, the path of the dynamic linker that should be used is embedded at link time into the .interp section of the executable's PT_INTERP segment. In those systems, dynamically loaded shared libraries can be identified by the filename suffix .so (shared object).

The dynamic linker can be influenced into modifying its behavior during either the program's execution or the program's linking, and the examples of this can be seen in the run-time linker manual pages for various Unix-like systems.[2][3][4][5][6] A typical modification of this behavior is the use of LD_LIBRARY_PATH and LD_PRELOAD environment variables, which adjust the runtime linking process by searching for shared libraries at alternate locations and by forcibly loading and linking libraries that would otherwise not be, respectively. An example is zlibc,[7] also known as uncompress.so,[a] which facilitates transparent decompression when used through the LD_PRELOAD hack; consequently, it is possible to read pre-compressed (gzipped) file data on BSD and Linux systems as if the files were not compressed, essentially allowing a user to add transparent compression to the underlying filesystem, although with some caveats. The mechanism is flexible, allowing trivial adaptation of the same code to perform additional or alternate processing of data during the file read, prior to the provision of said data to the user process that has requested it.[8][9]

macOS and iOS

[edit]

In the Apple Darwin operating system, and in the macOS and iOS operating systems built on top of it, the path of the dynamic linker that should be used is embedded at link time into one of the Mach-O load commands in the executable image. In those systems, dynamically loaded shared libraries can be identified either by the filename suffix .dylib or by their placement inside the bundle for a framework.

The dynamic linker not only links the target executable to the shared libraries but also places machine code functions at specific address points in memory that the target executable knows about at link time. When an executable wishes to interact with the dynamic linker, it simply executes the machine-specific call or jump instruction to one of those well-known address points. The executables on the macOS and iOS platforms often interact with the dynamic linker during the execution of the process; it is even known that an executable might interact with the dynamic linker, causing it to load more libraries and resolve more symbols, hours after it initially launches. The reason that a macOS or iOS program interacts with the dynamic linker so often is due both to Apple's Cocoa and Cocoa Touch APIs and Objective-C, the language in which they are implemented (see their main articles for more information).

The dynamic linker can be coerced into modifying some of its behavior; however, unlike other Unix-like operating systems, these modifications are hints that can be (and sometimes are) ignored by the dynamic linker. Examples of this can be seen in dyld's manual page.[10] A typical modification of this behavior is the use of the DYLD_FRAMEWORK_PATH and DYLD_PRINT_LIBRARIES environment variables. The former of the previously mentioned variables adjusts the executables' search path for the shared libraries, while the latter displays the names of the libraries as they are loaded and linked.

Apple's macOS dynamic linker is an open-source project released as part of Darwin and can be found in the Apple's open-source dyld project.[11]

XCOFF-based Unix-like systems

[edit]

In Unix-like operating systems using XCOFF, such as AIX, dynamically-loaded shared libraries use the filename suffix .a.

The dynamic linker can be influenced into modifying its behavior during either the program's execution or the program's linking. A typical modification of this behavior is the use of the LIBPATH environment variable. This variable adjusts the runtime linking process by searching for shared libraries at alternate locations and by forcibly loading and linking libraries that would otherwise not be, respectively.

OS/360 and successors

[edit]

Dynamic linking from Assembler language programs in IBM OS/360 and its successors is done typically using a LINK macro instruction containing a Supervisor Call instruction that activates the operating system routines that makes the library module to be linked available to the program. Library modules may reside in a "STEPLIB" or "JOBLIB" specified in control cards and only available to a specific execution of the program, in a library included in the LINKLIST in the PARMLIB (specified at system startup time), or in the "link pack area" where specific reentrant modules are loaded at system startup time.

Multics

[edit]

In the Multics operating system all files, including executables, are segments. A call to a routine not part of the current segment will cause the system to find the referenced segment, in memory or on disk, and add it to the address space of the running process. Dynamic linking is the normal method of operation, and static linking (using the binder) is the exception.

Efficiency

[edit]

Dynamic linking is generally slower (requires more CPU cycles) than linking during compilation time,[12] as is the case for most processes executed at runtime. However, loading time can be reduced by using "lazy linking", a process in which calls to functions of a library are linked to the implementation (inside the library) only when the first call occurs; a side effect is that, once the program is loaded, the first call to a function would be slower, as the time to link to it was shifted from startup time to run time.

However, dynamic linking is often more space-efficient (on disk and in memory at runtime). When a library is linked statically, every process being run is linked with its own copy of the library functions being called upon. Therefore, if a library is called upon many times by different programs, the same functions in that library are duplicated in several places in the system's memory. Using shared, dynamic libraries means that, instead of linking each file to its own copy of a library at compilation time and potentially wasting memory space, only one copy of the library is ever stored in memory at a time, freeing up memory space to be used elsewhere.[13] Additionally, in dynamic linking, a library is only loaded if it is actually being used.[14]

See also

[edit]

Notes

[edit]

References

[edit]

Further reading

[edit]
[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
A dynamic linker, also known as a runtime linker or dynamic loader, is the component of an operating system that loads shared libraries and resolves their symbols into the of an executing program at runtime, rather than during compilation. This process enables multiple applications to share the same library code in memory, promoting modularity and resource efficiency in modern computing environments. The origins of dynamic linking trace back to early multiprogramming systems like in the 1960s, where it facilitated loading external code segments into running processes without recompilation. However, widespread adoption in systems occurred in the late , with 4.0 introducing a comprehensive framework for shared libraries in 1988, using (PIC) to allow libraries to be mapped into arbitrary memory locations via the . This design, which required no kernel modifications and was transparent to developers, influenced subsequent implementations in BSD, , and other platforms, emphasizing benefits like reduced disk and memory usage—for instance, saving approximately 24 KB per program in typical setups. At runtime, the dynamic linker operates by first being invoked by the operating system kernel upon detecting an executable with a program interpreter entry (e.g., PT_INTERP in ELF format), such as /lib64/ld-linux-x86-64.so.2 on Linux systems. It then parses the executable's dependencies listed in DT_NEEDED entries, loads the required shared objects into memory, and applies relocations to patch symbolic references with actual addresses, often using structures like the Global Offset Table (GOT) and Procedure Linkage Table (PLT) for efficient, lazy binding that defers resolution until a function is first called. Environment variables like LD_LIBRARY_PATH can influence search paths, while features such as LD_PRELOAD allow overriding libraries for debugging or testing, and security mechanisms like Address Space Layout Randomization (ASLR) and RELRO (read-only relocations) enhance protection against exploits by randomizing load addresses and locking modifiable sections post-resolution. Implementations vary across operating systems but follow similar principles. In , the GNU C Library () provides the ld.so dynamic linker for binaries, handling dependency resolution and interposition. On macOS and , dyld serves as the dynamic linker for executables, supporting features like two-level namespaces for symbol lookup and environment variables prefixed with DYLD_ to control loading behavior. In AIX from , the loader resolves s at load time for shared objects, integrating with the system's runtime environment to share library pages across processes. Microsoft employs dynamic linking through DLLs, where the loader (part of ntdll.dll and the executive) binds imports at load or run time, though the explicit "dynamic linker" terminology is more common in contexts. Dynamic linking offers significant advantages, including smaller sizes, reduced disk storage by avoiding code duplication, and the ability to update libraries system-wide without relinking applications, which improves and can enhance through shared memory pages that minimize page faults in multi-process environments. Lazy binding further optimizes startup times by postponing non-essential resolutions. However, it introduces challenges such as a minor performance overhead from (e.g., about 8 machine cycles per reference due to ), potential increases in page faults from reduced locality, and runtime dependencies that can lead to compatibility issues if libraries are updated incompatibly or become unavailable. To mitigate these, practices like semantic versioning and symbol versioning—pioneered in Solaris in 1995 and adopted in —track /ABI changes, ensuring .

Fundamentals

Definition and Purpose

A dynamic linker, also known as a dynamic loader or runtime linker, is a specialized component of an operating system that loads shared object files—such as .so files on Unix-like systems or .dll files on Windows—into a running process's memory space and resolves references to external symbols at runtime. This process involves interpreting the executable's program headers to identify dependencies, mapping the required libraries into address space, and performing necessary relocations to enable the program to access the shared code and data. Unlike static linking, which embeds all dependencies at compile time, the dynamic linker operates during program execution, typically invoked as the program's interpreter via mechanisms like the PT_INTERP entry in the executable format. The primary purpose of a dynamic linker is to facilitate code and data sharing among multiple processes, thereby reducing overall consumption and requirements in multi-program environments. By loading shared libraries only once into and allowing multiple applications to reference them, it promotes efficient resource utilization, particularly in systems with limited hardware. Additionally, dynamic linking supports modular by enabling independent updates to libraries without necessitating recompilation or relinking of dependent applications, as long as interface compatibility is maintained; this eases and fosters architectures like plugins, where extensions can be loaded on demand. Key benefits include enhanced system through lazy symbol resolution—where bindings occur only when functions are first called—and the ability to support that can be shared across processes with varying address mappings. The concept of dynamic linking emerged in the late 1960s as an innovation in early systems to overcome the limitations of monolithic, statically linked executables that duplicated code across processes and hindered efficient sharing. Pioneered in , a collaborative project between MIT, , and starting in 1965, it allowed processes to dynamically incorporate external segments containing routines, resolving symbols via traps upon first reference to enable fine-grained among users. This approach addressed resource constraints in multiprogrammed environments and influenced subsequent systems; by the 1980s, it evolved into mechanisms in Unix variants, notably with in 1988, which extended sharing to library routines like standard I/O functions to further optimize memory and I/O efficiency.

Comparison to Static Linking

Static linking involves embedding all necessary code directly into the file during the compilation and linking phase, producing a self-contained binary that requires no external dependencies at runtime. This process resolves all symbols and relocations at link time, ensuring the final includes complete copies of the required functions. In contrast, dynamic linking defers symbol resolution and binding until runtime, using shared libraries that multiple executables can without duplication. This late binding allows the operating system loader to map libraries into memory as needed, whereas static linking performs early binding at , resulting in larger executables but eliminating searches. Key trade-offs include static linking's simplicity and predictability versus dynamic linking's flexibility in resource sharing. Dynamic linking offers advantages over static linking, such as reduced disk space and memory usage through sharing across applications, potentially saving virtual storage when multiple processes run concurrently. It also facilitates easier patching of without recompiling or redistributing executables, as updates to shared propagate system-wide. However, dynamic linking incurs runtime overhead, including additional machine cycles (approximately 8 per reference) for through "" and potential increases in page faults due to reduced code locality. It can also lead to "," where incompatible versions or missing dependencies cause runtime failures, complicating deployment in diverse environments. Static linking suits use cases like embedded systems or standalone applications, where self-containment ensures reliability without external dependencies, as seen in microcontrollers and IoT devices. Dynamic linking is preferred in general-purpose operating system environments, such as systems, to optimize space and enable shared updates across numerous applications.

Operational Mechanism

Library Loading Process

The library loading process in dynamic linking is initiated either at program startup or during runtime. When a dynamically linked is launched, the operating system invokes the dynamic linker as part of the execution mechanism, such as through the execve in systems, where the linker is specified in the executable's interpreter section (e.g., .interp in ELF files). Alternatively, libraries can be loaded dynamically at runtime via explicit calls like dlopen(), allowing programs to incorporate additional shared objects on demand. The dynamic linker begins by parsing the executable's dependency list, typically stored in headers such as the DT_NEEDED entries in the ELF dynamic section, which enumerate required shared libraries by name (e.g., libc.so.6). It then searches for these libraries using a prioritized path resolution strategy, starting with embedded paths in the executable (e.g., DT_RPATH), followed by environment variables like LD_LIBRARY_PATH, cached library indices (e.g., /etc/ld.so.cache), and standard directories such as /lib and /usr/lib. To handle recursive dependencies, the linker performs a depth-first or breadth-first traversal, building a complete dependency graph while detecting and avoiding cycles by tracking already-loaded libraries in a link chain, ensuring each unique library is processed only once. Once located, the linker maps the shared libraries into the process's virtual memory space using mechanisms like mmap to load file segments as shared, read-only regions, which promotes efficient memory sharing across processes. It allocates distinct segments for code (text), initialized data, and uninitialized data (BSS), positioning them at preferred base addresses specified in the library's ELF headers to support position-independent code (PIC) and address space layout randomization (ASLR). This mapping occurs in reverse dependency order—loading leaf dependencies before their dependents—to establish a stable foundation before integrating higher-level libraries. A notable feature in Unix-like systems is the LD_PRELOAD environment variable, which allows users to specify additional shared libraries to be loaded before all others during the library loading process. This mechanism enables function interposition, where definitions in the preloaded library can override those in other libraries or the executable, allowing interception, modification, or monitoring of standard library calls without altering the application's source code. Legitimate use cases for LD_PRELOAD include debugging, instrumentation for performance analysis, providing compatibility layers for legacy software, enforcing security policies, and controlling resource access. However, it also carries security implications, as it can be abused to inject malicious code that alters program behavior, potentially leading to privilege escalation or data exfiltration. Consequently, LD_PRELOAD is typically disabled or restricted in setuid binaries and hardened environments to mitigate these risks. Understanding LD_PRELOAD is crucial for systems programming, as well as for security analysis and defense against such exploitation techniques. Errors during loading are handled by terminating the process with diagnostic messages, such as failures due to missing libraries (e.g., "cannot open shared object file") or invalid paths in LD_LIBRARY_PATH, which may be ignored in secure-execution modes to prevent exploitation. In Linux systems, such errors can often be temporarily resolved by using the ldd command to identify the required library paths and then setting the LD_LIBRARY_PATH environment variable to include those directories before executing the program, for example, by running export LD_LIBRARY_PATH=/path/to/library/dir:$LD_LIBRARY_PATH followed by the executable. For persistent configuration, the path can be added to shell configuration files such as ~/.bashrc. Version mismatches, detected via sonames or symbol versioning in the headers, also trigger failures to ensure compatibility. Following successful loading, the process proceeds to symbol resolution, where references between the executable and libraries are connected.

Symbol Resolution

In dynamic linking, symbol resolution is the process by which the dynamic linker identifies and binds undefined references in an or to their corresponding definitions in loaded shared objects. This occurs after libraries are loaded into memory, ensuring that external symbols—such as function calls or variable accesses—are correctly mapped without requiring full static resolution at . The linker traverses the dependency chain, starting from the main 's dependencies listed in the DT_NEEDED entries of the .dynamic section, to locate definitions systematically. Symbols resolved by the dynamic linker primarily include external functions and variables, which are stored in dedicated symbol tables within object files. In ELF-based systems, the .dynsym section holds these dynamic symbols, each entry containing the symbol name (via an offset into the .dynstr string table), type (e.g., STT_FUNC for functions or STT_OBJECT for variables), binding (e.g., STB_GLOBAL for external visibility), and scope information such as section index. These tables enable the linker to distinguish between local symbols (confined to a single object) and global ones (potentially shared across objects), facilitating efficient lookups during runtime. The resolution strategy employs a through the loaded objects' symbol tables, prioritizing dependencies in the order specified by DT_NEEDED tags to avoid circular . For fast lookup, the linker uses hash tables referenced by the DT_HASH tag in the .dynamic section, which map symbol names to table indices, reducing search time from linear to near-constant. When multiple definitions exist, symbols (with STB_GLOBAL binding and non-weak attributes) take precedence over weak symbols (STB_WEAK), which serve as optional placeholders and can be overridden without error; if no definition is found, a weak one is used, or the remains unresolved if none exists. This handling ensures compatibility in library updates where weak symbols allow graceful fallbacks. Binding modes determine when and how symbols are resolved: load-time binding (eager resolution) processes all undefined symbols immediately upon loading, often enforced via the LD_BIND_NOW for security or predictability, though it increases startup latency. In contrast, runtime binding (lazy resolution) defers resolution until the first reference, typically via the Procedure Linkage Table (PLT) and Global Offset Table (GOT), where an initial indirect call triggers the linker to patch the address on-demand, optimizing performance by skipping unused symbols. Global binding allows symbols to be shared across the process, while local binding restricts them to the defining object, preventing unintended interposition. Programmers can perform manual symbol resolution using APIs like dlsym(), which searches for a symbol by name within a specified handle (e.g., from dlopen()) or the default search order (RTLD_DEFAULT), returning its runtime address or NULL if unresolved. This is useful for plugins or conditional loading, with errors retrievable via dlerror(). To control symbol export and visibility, attributes such as attribute((visibility("hidden"))) in GCC hide symbols from the dynamic symbol table, preventing external resolution and reducing namespace pollution; other modes like "protected" allow local overrides while keeping global visibility. These features enhance modularity and security in shared libraries.

Relocation and Initialization

After symbol resolution, the dynamic linker processes relocation entries to adjust references in the loaded code and data sections, patching them with the actual runtime addresses of symbols. These relocations ensure that the executable and shared libraries function correctly regardless of their load addresses in memory. Relocation types fall into two primary categories: absolute and relative. Absolute relocations, such as R_X86_64_64 on systems, directly fix the target address to a specific absolute location in memory, requiring updates to the loaded object's base address. In contrast, relative relocations, like R_X86_64_PC32, compute offsets relative to the position of the reference itself, avoiding the need for absolute address fixes and enabling (PIC). In ELF-based systems, these entries are stored in tables such as .rela.dyn for dynamic relocations, where each entry specifies the location to patch, the relocation type, the associated symbol, and an addend for offset calculations. The relocation process supports for non-immediate references, such as indirect function calls, deferring patches until the first use to improve startup performance, though full eager relocation can be enforced for . (PIC) is a key technique in dynamic linking, allowing shared libraries to be loaded and shared across multiple processes at arbitrary addresses without modification. This supports features like (ASLR) for and efficient memory sharing. In PIC implementations, the global offset table (GOT) stores resolved addresses for data symbols, while the procedure linkage table (PLT) handles function calls through indirect jumps, enabling runtime resolution without altering the code itself. Following relocations, the dynamic linker performs initialization by executing constructors—functions that set up library state—typically stored in sections like .init for legacy code or .init_array for modern binaries. These constructors run in dependency order before transferring control to the main program. On library unload, corresponding destructors in sections such as .fini or .fini_array are invoked in reverse order to clean up resources.

Implementations

Microsoft Windows

In Microsoft Windows, dynamic linking is implemented through the (PE) file format, derived from the Common Object File Format (COFF), which supports both executables (.exe) and dynamic-link libraries (DLLs). The PE format includes sections such as the import directory table in the .idata section, which lists dependencies on external DLLs and the functions imported from them. The operating system's loader, primarily handled by ntdll.dll and kernel32.dll, processes these imports during program execution to resolve and load required modules dynamically. The loading process begins when the Windows loader parses the PE header of an , identifying DLL dependencies via the import table in the .idata section. For each listed DLL, the loader calls internal functions like LdrLoadDll in ntdll.dll to map the module into the process's , followed by updating the Import Address Table (IAT) with actual function addresses using routines such as LdrpSnapIAT. Runtime loading is facilitated by the LoadLibrary in kernel32.dll, which allows explicit loading of DLLs on demand, enabling flexible dependency management beyond initial startup. Windows also supports delay-load imports, where function resolution is postponed until the first call, reducing startup time by avoiding unnecessary loads; this is achieved through compiler directives and linker options that wrap imports with stubs calling GetProcAddress for lazy binding. A distinctive feature of Windows dynamic linking is support for side-by-side assemblies, introduced in to enable multiple versions of the same DLL to coexist without conflicts, addressing DLL hell issues from earlier systems. These assemblies are groups of DLLs, resources, and metadata deployed together, allowing applications to bind to specific versions at runtime. Dependency declaration occurs via manifest files, XML documents embedded in executables or separate files, which specify required assemblies by name, version, and public key token, ensuring the loader selects the correct side-by-side version during resolution. Historically, dynamic linking in Windows evolved with the Win32 subsystem introduced in in 1993, building on earlier 16-bit DLL support in but adopting the PE/COFF format for 32-bit portability across processors. This foundation persisted through subsequent versions, with enhancements like side-by-side assemblies in 2001 and, in modern (UWP) apps introduced in in 2012, integration with AppX packaging for isolated deployment of DLL dependencies within app containers, maintaining PE-based loading while adding sandboxing.

ELF-based Systems

In ELF-based systems, such as those found in various operating systems, dynamic linking is facilitated by the (ELF), which standardizes the structure for executables and shared libraries. The primary dynamic linker, often referred to as ld.so or variants like ld-linux.so, is responsible for loading shared objects, resolving dependencies, and preparing the program for execution at runtime. This ensures efficient sharing of and among multiple programs while allowing for modular updates to libraries. The dynamic linker is invoked by the kernel following an exec() system call on an ELF executable that specifies it via the PT_INTERP program header, typically pointing to a path like /lib/ld-linux.so.2. Upon invocation, the linker maps the executable into memory and examines its PT_DYNAMIC program header, which contains the .dynamic section. This section holds an array of dynamic entries (ElfXX_Dyn structures) that provide essential metadata for linking. Key to dependency management in the .dynamic section are the DT_NEEDED tags, which list the names of required shared libraries as offsets into the string table (DT_STRTAB); these entries dictate the order in which dependencies are loaded in a breadth-first manner. Shared libraries are identified and versioned using sonames, specified via the DT_SONAME tag—for instance, libc.so.6 indicates the GNU C Library version 6—allowing the linker to select compatible implementations without embedding full paths. To locate these libraries, the linker interprets search paths from DT_RPATH (a colon-separated list embedded at link time, applicable to the entire dependency tree) or its modern successor DT_RUNPATH (limited to direct dependencies), supplemented by environment variables like LD_LIBRARY_PATH and system caches. For troubleshooting library loading issues, such as when a shared library cannot be found, the ldd command can be used to display the shared libraries required by an executable and their resolved paths. If a library is not located in the standard search paths, temporarily setting the LD_LIBRARY_PATH environment variable to include the directory containing the library can resolve the error. For persistent configuration, this variable can be added to the user's shell profile, such as ~/.bashrc. This mechanism is implemented in major ELF-based systems, including distributions utilizing the GNU C Library's ld-linux.so (part of ) and with its runtime linker ld.so.1. In , tools like ldd simulate the loading process to list an executable's dependencies by setting the LD_TRACE_LOADED_OBJECTS , aiding in verification without execution. Solaris employs similar conventions but integrates with its own linker tools for cache management via /etc/ld.so.conf. These implementations support relocation types for address adjustments, as outlined in the broader operational mechanisms of dynamic linking.

Mach-O in Apple Systems

In Apple systems such as macOS and iOS, the dynamic linker is implemented as dyld (with the current version being dyld3, introduced in 2017 and default since macOS 10.13 for system apps and macOS 10.15 for third-party apps), a standalone binary located at /usr/lib/dyld that serves as the primary loader for Mach-O executables, dynamic libraries, and bundles. dyld3 represents a complete rewrite of the dynamic linker, featuring out-of-process Mach-O parsing, launch closure caching to disk for faster startups, and enhanced security through improved randomization, while maintaining compatibility with prior versions. dyld integrates closely with the Mach kernel's virtual memory management, enabling address space layout randomization (ASLR) through features like image sliding, where loaded modules are mapped at randomized virtual addresses to enhance security. Upon process launch, the kernel passes control to dyld after initial executable loading, at which point dyld parses the Mach-O header and resolves dependencies before transferring execution to the main program entry point. The file format, used exclusively in Apple ecosystems for binaries, incorporates specific load commands to declare dynamic dependencies, primarily through the LC_LOAD_DYLIB command, which specifies the path and compatibility version of required dynamic libraries (dylibs). This command is part of the load command array in the header, allowing dyld to identify and load shared libraries at runtime, with variants like LC_LOAD_WEAK_DYLIB for optional dependencies that do not cause fatal errors if unresolved. also supports modular loading via bundles (MH_BUNDLE file type), which are dynamically loadable code modules often used for plug-ins, and frameworks, which package libraries, headers, and resources into self-contained units for easier distribution and versioning. Symbol resolution in dyld employs a two-level namespace model by default, where symbols are qualified by both their name and the originating library's identifier (e.g., libraryName!symbolName), enabling precise versioning and avoiding flat namespace collisions across multiple libraries. This approach supports compatibility by allowing applications to bind to specific library versions at link time, with dyld enforcing these bindings during loading unless overridden by flags like -flat_namespace. To optimize startup , dyld utilizes the dyld_shared_cache, a precomputed, system-wide cache of commonly used libraries (e.g., system frameworks like Foundation and CoreFoundation) that are slide-compacted and mapped once into , reducing load times and memory overhead for multiple processes. Apple systems enforce strict security measures in dynamic linking, particularly through , where all binaries must be digitally signed with a valid certificate to verify origin and integrity before dyld loads them. In macOS, starting from version 10.10.4, policy restricts linking to external dylibs outside the app bundle or standard system paths (e.g., /usr/lib), rejecting unsigned or mismatched signatures to prevent tampering. On , runtime loading of dynamic libraries is further restricted for security; dyld prohibits arbitrary dlopen calls to unsigned or non-embedded code, confining loading to pre-approved, signed frameworks within the app bundle to mitigate jailbreak exploits and unauthorized . These policies integrate with the hardened runtime, ensuring that library validation occurs during dyld's binding phase.

Other and Historical Implementations

In the 1960s and 1970s, pioneered dynamic linking through its segmented system, allowing procedures and data to be loaded on demand and bound at runtime. The operating system used segmentation to enable direct addressing of information across files and memory, with dynamic linking facilitating references between compilation units via entry points and linkage sections. This approach supported shared procedures and data without requiring full relinking, using indirect calls through linkage segments to resolve bindings dynamically. Early systems like OS/360 introduced dynamic loading concepts via the linkage editor, which processed object modules into load modules while supporting overlays for memory-constrained environments. The Batch Loader (BLDL) allowed on-demand loading of overlay segments during execution, evolving in (Multiple Virtual Storage) to more sophisticated mechanisms. By the time of z/OS, this had advanced to Dynamic Link Libraries (DLLs) within the Language Environment, where the binder and loader handle runtime linking of reusable modules, maintaining compatibility with legacy OS/360 formats. AIX employs the XCOFF (eXtended Common Object File Format) for dynamic linking, utilizing the ld command as a binder to create modules with a dedicated .loader section. This section includes headers, symbol tables, relocation entries, and import file IDs to specify dependencies on shared libraries, enabling the loader to resolve symbols at runtime. Shared libraries are located via the LIBPATH or paths embedded in the loader section, supporting and deferred binding for efficiency. Files flagged with F_DYNLOAD or F_SHROBJ are designed for , with the (TOC) aiding in external reference resolution. BeOS and its successor Haiku adapted the ELF format for dynamic linking, treating add-ons as loadable modules with standard program headers for code (.text), data (.data), and dynamic sections. Haiku's implementation combines segments into loadable units with specific protection flags (read-execute for code, read-write for data), allowing runtime loading of filesystem drivers and other extensions while maintaining ELF compatibility. This variant supports position-independent execution, though early versions addressed architecture-specific issues like combined read-write-execute permissions on PowerPC. Plan 9 from Bell Labs opted for static linking in its binaries to simplify distribution and avoid runtime dependencies, forgoing traditional dynamic libraries in favor of a unified . While it lacks a conventional dynamic linker, libraries like libthread enable runtime thread creation and management, providing a form of modular loading for concurrent programming without full dynamic symbol resolution.

Efficiency and Optimizations

Performance Techniques

Dynamic linkers incorporate several techniques to mitigate the overhead associated with library loading, symbol resolution, and relocation, focusing on reducing startup latency and memory usage across diverse systems. These optimizations address the inherent costs of dynamic linking, such as disk I/O for loading shared objects and computational expenses in address adjustments, without compromising core functionality. Preloading shared libraries into memory ahead of time serves as a key strategy to bypass repeated disk accesses for commonly used objects. In , the allows loading specified shared libraries before others, enabling function interposition to selectively override functions in other shared objects—a technique detailed in the Operational Mechanism section. This early loading can have the of reducing I/O for those specific libraries, thereby improving performance. However, due to security implications, such as the potential for abuse in altering program behavior and exploitation in privileged contexts, LD_PRELOAD is commonly disabled or restricted in setuid binaries and hardened environments. For broader performance benefits in environments with frequent library reuse, system-wide configurations like /etc/ld.so.preload or the ldconfig cache are used to preload or index common libraries, minimizing search and I/O bottlenecks during application startup. Address Space Layout Randomization (ASLR) introduces performance trade-offs by randomizing load addresses to enhance security against exploits, necessitating runtime relocations that increase startup costs. Systems mitigate this through (PIC), which compiles libraries to operate at arbitrary addresses with fewer adjustments, though PIC imposes some runtime overhead on 32-bit architectures. Full ASLR without PIC can elevate relocation expenses significantly on resource-constrained setups. Caching mechanisms accelerate symbol resolution by storing lookup results for subsequent use. Runtime symbol caches, exemplified by NetBSD's negative symbol cache, track unresolved symbols to skip futile searches in future loads. In legacy macOS implementations, prebinding fixed library addresses during installation, permitting direct symbol references and reducing launch times by pre-resolving relocations, until invalidated by system updates requiring a full rebind. Performance evaluation of dynamic linking relies on specialized tools to quantify overheads like startup delays from the process. On , traces system calls during library loading, revealing I/O and resolution latencies in heavily linked programs. Apple's Instruments profiler, conversely, instruments dyld operations on macOS to dissect binding and caching phases, aiding identification of bottlenecks in multi-library environments.

Lazy Binding and Loading

Lazy binding defers the resolution of external symbols until their first use, in contrast to eager binding, which resolves all symbols at load time. This approach minimizes initial program startup latency by avoiding unnecessary overhead for symbols that may never be referenced. In ELF-based systems, lazy binding is the default behavior and is implemented using the Procedure Linkage Table (PLT) and Global Offset Table (GOT). The PLT contains stubs for external function calls; on the first invocation, these stubs redirect execution to the dynamic linker, which then resolves the symbol's address and updates the corresponding GOT entry for subsequent direct access. Lazy loading extends this deferral to the libraries themselves, loading shared objects only when explicitly required rather than at program startup. In systems, the dlopen function with the RTLD_LAZY flag enables this by performing lazy binding for symbols within the loaded library, resolving them only as they are executed. Similarly, on Windows, the linker supports delay-loaded DLLs through directives like #pragma(comment(lib, "delayimp.lib")), which postpone loading until a function from the DLL is first called, often using LoadLibrary internally. These mechanisms allow applications to avoid loading infrequently used libraries upfront, reducing and initialization time. The implementation relies on indirect jumps and runtime interception: in ELF, the PLT's initial entries point to the dynamic linker's resolver, which handles symbol lookup via the program's symbol table and updates the GOT atomically to prevent race conditions. While some historical or specialized systems use page faults or signal handlers to trap unresolved references and invoke the linker, modern implementations favor explicit PLT/GOT redirection for predictability and efficiency. This deferred strategy offers significant benefits, such as accelerated program launch times—particularly for large applications with many dependencies—by skipping resolution for unused code paths, thereby improving overall resource utilization. However, it introduces drawbacks, including potential runtime pauses during the first resolution, which can affect real-time performance, and the risk of deferred errors surfacing unpredictably if a cannot be resolved later. A similar principle applies in the (JVM), where class loading and linking occur on-demand during execution, analogous to lazy binding to optimize startup and memory use in dynamic environments.

References

Add your contribution
Related Hubs
User Avatar
No comments yet.