Recent from talks
Nothing was collected or created yet.
Dynamic linker
View on WikipediaIn 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]- ^ Microsoft Corporation. "Creating a Resource-Only DLL". Microsoft Developer Network Library.
- ^ : Solaris dynamic linker/loader – Solaris 11.4 User Commands Reference Manual
- ^ – Linux Programmer's Manual – Administration and Privileged Commands from Manned.org
- ^ : FreeBSD dynamic linker/loader – FreeBSD General Commands Manual
- ^ : NetBSD dynamic linker/loader – NetBSD General Commands Manual
- ^ : OpenBSD dynamic linker/loader – OpenBSD General Commands Manual
- ^ "ZLIBC - Transparent access to compressed files". Archived from the original on 2000-06-04.
- ^ "uncompress.so". delorie.com. Retrieved 2014-07-04.
- ^ "zlibc.conf". delorie.com. Retrieved 2014-07-04.
- ^ : Darwin/Mac OS X dynamic linker/loader – Darwin and macOS General Commands Manual
- ^ Apple Inc. "Open Source - Releases". apple.com. Retrieved 2014-07-04.
- ^ Xuxian, Jiang (2009). "Operating Systems Principles: Linking and Loading" (PDF). North Carolina State University. Retrieved 2020-09-24.
- ^ Jones, M. (2008-08-28). "Anatomy of Linux dynamic libraries". IBM. Retrieved 2020-09-24.
- ^ Sivilotti, Paul (August 2012). "Dynamic Linking and Loading" (PDF). Ohio State University. Retrieved 2020-09-24.
Further reading
[edit]- Levine, John R. (2000) [October 1999]. Linkers and Loaders. The Morgan Kaufmann Series in Software Engineering and Programming (1 ed.). San Francisco, USA: Morgan Kaufmann. ISBN 1-55860-496-0. OCLC 42413382. Archived from the original on 2012-12-05. Retrieved 2020-01-12. Code: [1][2][dead link] Errata: [3]
External links
[edit]- Dynamic Linking and Loading, IECC.com
- Dynamic Linking in Linux and Windows, part one, Symantec.com
- Anatomy of Linux dynamic libraries, IBM.com
Dynamic linker
View on GrokipediaFundamentals
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.[6] 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.[7] 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.[6] The primary purpose of a dynamic linker is to facilitate code and data sharing among multiple processes, thereby reducing overall memory consumption and disk storage requirements in multi-program environments.[2] By loading shared libraries only once into memory and allowing multiple applications to reference them, it promotes efficient resource utilization, particularly in systems with limited hardware.[7] Additionally, dynamic linking supports modular software design by enabling independent updates to libraries without necessitating recompilation or relinking of dependent applications, as long as interface compatibility is maintained; this eases maintenance and fosters architectures like plugins, where extensions can be loaded on demand.[6] Key benefits include enhanced system performance through lazy symbol resolution—where bindings occur only when functions are first called—and the ability to support position-independent code that can be shared across processes with varying address mappings.[7] The concept of dynamic linking emerged in the late 1960s as an innovation in early time-sharing systems to overcome the limitations of monolithic, statically linked executables that duplicated code across processes and hindered efficient sharing.[8] Pioneered in Multics, a collaborative project between MIT, Bell Labs, and General Electric starting in 1965, it allowed processes to dynamically incorporate external segments containing routines, resolving symbols via traps upon first reference to enable fine-grained code reuse among users.[8] This approach addressed resource constraints in multiprogrammed environments and influenced subsequent systems; by the 1980s, it evolved into shared library mechanisms in Unix variants, notably with SunOS in 1988, which extended sharing to library routines like standard I/O functions to further optimize memory and I/O efficiency.[2]Comparison to Static Linking
Static linking involves embedding all necessary library code directly into the executable file during the compilation and linking phase, producing a self-contained binary that requires no external dependencies at runtime.[9] This process resolves all symbols and relocations at link time, ensuring the final executable includes complete copies of the required library functions.[10] In contrast, dynamic linking defers symbol resolution and binding until runtime, using shared libraries that multiple executables can reference without duplication.[9] This late binding allows the operating system loader to map libraries into memory as needed, whereas static linking performs early binding at compile time, resulting in larger executables but eliminating runtime library searches.[10] Key trade-offs include static linking's simplicity and predictability versus dynamic linking's flexibility in resource sharing.[9] Dynamic linking offers advantages over static linking, such as reduced disk space and memory usage through library sharing across applications, potentially saving virtual storage when multiple processes run concurrently.[10] It also facilitates easier patching of libraries without recompiling or redistributing executables, as updates to shared libraries propagate system-wide.[9] However, dynamic linking incurs runtime overhead, including additional machine cycles (approximately 8 per reference) for indirection through "glue code" and potential increases in page faults due to reduced code locality.[10] It can also lead to "dependency hell," where incompatible library versions or missing dependencies cause runtime failures, complicating deployment in diverse environments.[9] 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.[11] Dynamic linking is preferred in general-purpose operating system environments, such as Unix-like systems, to optimize space and enable shared updates across numerous applications.[10]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 executable is launched, the operating system invokes the dynamic linker as part of the execution mechanism, such as through theexecve system call in Unix-like systems, where the linker is specified in the executable's interpreter section (e.g., .interp in ELF files).[12] Alternatively, libraries can be loaded dynamically at runtime via explicit calls like dlopen(), allowing programs to incorporate additional shared objects on demand.[13]
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).[1] 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.[12] 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.[13][1]
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.[12] 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).[1] This mapping occurs in reverse dependency order—loading leaf dependencies before their dependents—to establish a stable foundation before integrating higher-level libraries.[1]
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.[12]
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.[14] Understanding LD_PRELOAD is crucial for systems programming, as well as for security analysis and defense against such exploitation techniques.[12]
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.[12] 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.[12][15] Version mismatches, detected via sonames or symbol versioning in the headers, also trigger failures to ensure compatibility.[13] 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 executable or shared library 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 compile time. The linker traverses the dependency chain, starting from the main executable's dependencies listed in the DT_NEEDED entries of the .dynamic section, to locate definitions systematically.[12][16] 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.[17][16] The resolution strategy employs a breadth-first search through the loaded objects' symbol tables, prioritizing dependencies in the order specified by DT_NEEDED tags to avoid circular references. 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, strong 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 strong definition is found, a weak one is used, or the reference remains unresolved if none exists. This handling ensures compatibility in library updates where weak symbols allow graceful fallbacks.[12][18][16] Binding modes determine when and how symbols are resolved: load-time binding (eager resolution) processes all undefined symbols immediately upon library loading, often enforced via the LD_BIND_NOW environment variable 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.[17][19][12] 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.[20][21][22]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.[23] These relocations ensure that the executable and shared libraries function correctly regardless of their load addresses in memory.[24] Relocation types fall into two primary categories: absolute and relative. Absolute relocations, such as R_X86_64_64 on x86-64 systems, directly fix the target address to a specific absolute location in memory, requiring updates to the loaded object's base address.[24] 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 position-independent code (PIC).[24] 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.[25] The relocation process supports lazy evaluation 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 security.[17] Position-independent code (PIC) is a key technique in dynamic linking, allowing shared libraries to be loaded and shared across multiple processes at arbitrary addresses without modification.[26] This supports features like address space layout randomization (ASLR) for security and efficient memory sharing.[27] 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.[17] 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 ELF binaries.[28] These constructors run in dependency order before transferring control to the main program.[29] On library unload, corresponding destructors in sections such as .fini or .fini_array are invoked in reverse order to clean up resources.[29]Implementations
Microsoft Windows
In Microsoft Windows, dynamic linking is implemented through the Portable Executable (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.[30][31] The loading process begins when the Windows loader parses the PE header of an executable, 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 address space, followed by updating the Import Address Table (IAT) with actual function addresses using routines such as LdrpSnapIAT. Runtime loading is facilitated by the LoadLibrary API 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.[31][32][33] A distinctive feature of Windows dynamic linking is support for side-by-side assemblies, introduced in Windows XP 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.[34][35] Historically, dynamic linking in Windows evolved with the Win32 subsystem introduced in Windows NT 3.1 in 1993, building on earlier 16-bit DLL support in Windows 3.0 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 Universal Windows Platform (UWP) apps introduced in Windows 8 in 2012, integration with AppX packaging for isolated deployment of DLL dependencies within app containers, maintaining PE-based loading while adding sandboxing.[30][36]ELF-based Systems
In ELF-based systems, such as those found in various Unix-like operating systems, dynamic linking is facilitated by the Executable and Linkable Format (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 process ensures efficient sharing of code and data among multiple programs while allowing for modular updates to libraries.[6] 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.[12][37] 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.[38][39][12] 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.[15][12] This mechanism is implemented in major ELF-based systems, including Linux distributions utilizing the GNU C Library's ld-linux.so (part of glibc) and Oracle Solaris with its runtime linker ld.so.1. In Linux, tools like ldd simulate the loading process to list an executable's dependencies by setting the LD_TRACE_LOADED_OBJECTS environment variable, 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.[40][12][15]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.[41][42] 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.[41] 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.[3] 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.[43]
The Mach-O 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).[44] This command is part of the load command array in the Mach-O 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.[44] Mach-O 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.[43]
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.[45] 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.[45] To optimize startup performance, 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 memory, reducing load times and memory overhead for multiple processes.[42]
Apple systems enforce strict security measures in dynamic linking, particularly through code signing, where all Mach-O binaries must be digitally signed with a valid certificate to verify origin and integrity before dyld loads them.[46] In macOS, starting from version 10.10.4, Gatekeeper 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.[46] On iOS, 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 code injection.[46] These policies integrate with the hardened runtime, ensuring that library validation occurs during dyld's binding phase.[46]
Other and Historical Implementations
In the 1960s and 1970s, Multics pioneered dynamic linking through its segmented virtual memory 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.[47] Early IBM 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) entry point allowed on-demand loading of overlay segments during execution, evolving in MVS (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.[48][49] AIX employs the XCOFF (eXtended Common Object File Format) for dynamic linking, utilizing theld command as a binder to create executable 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 system loader to resolve symbols at runtime. Shared libraries are located via the LIBPATH environment variable or paths embedded in the loader section, supporting position-independent code and deferred binding for efficiency. Files flagged with F_DYNLOAD or F_SHROBJ are designed for dynamic loading, with the Table of Contents (TOC) aiding in external reference resolution.[50]
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.[51]
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 namespace. 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.
