Search

Indirect Symbol

10 min read 0 views
Indirect Symbol

Introduction

An indirect symbol is a symbol that does not represent a concrete address or value in an object file or executable, but instead refers to another symbol or a relocation that will be resolved at load time or runtime. The concept is central to the operation of dynamic linkers and loaders on modern operating systems, enabling shared libraries, lazy binding, and runtime code relocation. Indirect symbols are typically represented in the symbol table of an object file by a special type or binding flag that indicates that the symbol should be resolved through the Global Offset Table (GOT), Procedure Linkage Table (PLT), or an equivalent mechanism. The term is used in several binary formats, including ELF on Linux and FreeBSD, PE/COFF on Windows, and Mach-O on macOS. Understanding how indirect symbols work is essential for developers who build or analyze low‑level binaries, implement custom linkers, or engage in reverse engineering and security research.

History and Background

Early Linkers and Symbol Resolution

In the earliest days of software distribution, object files contained only static references to functions and data. A linker performed a one‑time resolution of all symbols, producing a monolithic executable. Symbol resolution was straightforward: each reference was replaced by the absolute address of the target symbol. This approach limited modularity and required recompilation when libraries changed. The first portable object file format, a.out, introduced a symbol table but did not provide mechanisms for dynamic symbol resolution beyond simple relocations.

Emergence of Dynamic Linking

Dynamic linking emerged in the late 1980s and early 1990s as a solution to the binary size and update issues inherent in static linking. The System V Application Binary Interface (ABI) and the ELF format incorporated a dynamic symbol table that allowed executables to refer to symbols in shared libraries. The dynamic loader performs relocation at load time, resolving references that could not be determined at compile time. However, early implementations still performed eager binding: all dynamic references were resolved before control entered the program, which incurred a startup penalty for large applications with many shared library dependencies.

Definition of Indirect Symbols

Indirect symbols were introduced to mitigate the performance cost of eager binding. Rather than resolving a dynamic symbol immediately, the linker generates a relocation entry that points to an entry in the GOT or PLT. The symbol itself is marked as indirect, meaning that the actual address will be determined later, typically the first time the symbol is referenced. This lazy binding strategy defers the cost of address resolution until the symbol is actually used, which can reduce startup time for programs that do not exercise all imported functions. Indirect symbols are now a standard feature of ELF, PE/COFF, and other formats.

Key Concepts

Symbol Tables and Entries

All executable and object file formats expose a symbol table - a data structure containing entries that describe the names, addresses, types, and other attributes of functions, variables, and sections. Each symbol table entry typically includes fields for the symbol name, value (often the address or an offset), size, type, binding (local, global, weak), and visibility. In the ELF format, the type field is subdivided into a base type and a sub‑type; indirect symbols are usually indicated by the type STT_NOTYPE with a special binding or a relocation that refers to the GOT.

Relocation Entries and Types

Relocation entries are records that instruct the dynamic loader how to adjust addresses in the code or data sections. Each relocation specifies a location to be updated, the symbol it references, and the relocation type, which indicates the operation needed (e.g., add immediate, absolute address, or relative address). For indirect symbols, relocation types such as R_X86_64_JUMP_SLOT or R_X86_64_GLOB_DAT in ELF indicate that the relocation should point to the GOT or PLT. In PE/COFF, the IMAGE_REL_BASED_* series of relocation types fulfill a similar role.

Indirect Symbols in ELF

In the ELF format, indirect symbols are represented in the .dynsym section with a binding of STB_GLOBAL or STB_WEAK and a type of STT_NOTYPE or STT_FUNC, depending on the target. The relocation entries for lazy binding use R_X86_64_JUMP_SLOT (on x86_64) or the architecture‑specific equivalent. The dynamic linker populates the GOT entry with the address of the PLT stub the first time the symbol is invoked. Subsequent calls jump directly to the target address, bypassing the PLT and achieving near‑static performance.

Indirect Symbols in PE / COFF

In the PE/COFF format, indirect references are handled via the Import Address Table (IAT). Each entry in the IAT initially contains a null pointer or a placeholder. The loader writes the address of the imported function into the IAT entry upon first use. The symbol table entry for an imported function is marked with the IMAGE_SYM_DTYPE_IMPORT type, and the import directory entries reference the IAT. This mechanism is analogous to the ELF GOT/PLT approach but is implemented using different data structures.

PLT, GOT, and Lazy Binding

The Procedure Linkage Table (PLT) is a small code area that contains trampolines for calling imported functions. The Global Offset Table (GOT) holds addresses that the loader updates. When a program uses an imported function, it jumps to the PLT entry, which performs an indirect jump through the GOT. If the GOT entry is uninitialized, the PLT entry invokes the dynamic linker, which resolves the symbol, updates the GOT, and transfers control to the resolved address. After the first call, the PLT entry jumps directly to the resolved address, eliminating overhead. This pattern is common across architectures that support lazy binding.

Weak and Local Indirect Symbols

Weak symbols are optional references that can be overridden by strong symbols of the same name in another object. If a weak indirect symbol is not resolved by the time the program starts, the loader can treat it as null or zero. Local indirect symbols are symbols that are visible only within a translation unit but still may require relocation at runtime if they refer to external data. These concepts allow finer control over symbol resolution and provide flexibility for library authors to supply default implementations.

Technical Implementation

ELF Implementation Details

On ELF systems, the linker emits a relocation entry of type R_X86_64_JUMP_SLOT for each dynamic reference that should be lazily resolved. The relocation points to the PLT entry. The PLT stub, generated by the linker, contains a jump instruction that loads the address from the GOT and jumps to it. The GOT entry is initially zero; the dynamic linker writes the actual address during the first call. The relocation entry also records the symbol index, allowing the loader to look up the symbol in .dynsym. The ELF program header contains PT_DYNAMIC and PT_INTERP segments that instruct the loader about the dynamic section and interpreter path.

Windows Implementation Details

Windows uses a different strategy. Imported functions are referenced via the Import Table, which lists the DLL name and the ordinal or name of each function. The Import Address Table (IAT) contains placeholders that the loader fills with the actual addresses after resolving the DLLs. The loader also generates the thunk functions that forward calls through the IAT. If a function is not needed, its entry in the IAT may remain null. Additionally, Windows supports a feature called "IAT forwarding," where a function in one DLL can be forwarded to another, effectively making the first DLL an indirect symbol provider.

Other Formats (Mach‑O, a.out)

Mach‑O, used on macOS and iOS, employs a similar lazy binding mechanism through the indirect symbol table. Each indirect symbol entry refers to an address that is filled in by the dynamic loader. The Mach‑O header contains load commands such as LC_LOAD_DYLIB and LC_SEGMENT that describe shared library dependencies. The lazy binding process uses the dyld shared cache and the rebase information to resolve addresses at runtime. The a.out format, though largely obsolete, supported relocation entries for shared libraries but did not provide a dedicated indirect symbol mechanism; dynamic linking was typically performed via the external symbol table and relocations performed by the runtime loader.

Applications and Usage

Dynamic Libraries and Plugins

Indirect symbols are fundamental to plugin architectures where modules are loaded at runtime. A host application can load a plugin DLL or shared object that imports symbols from the host. By marking these imports as indirect, the plugin can defer resolution until the first use, allowing the host to modify or replace symbols after the plugin has been loaded. This dynamic dispatch mechanism is exploited by many modern frameworks, such as the GNU C Library (glibc) and the Qt framework, to provide extensible interfaces.

Runtime Linking and Performance

The lazy binding strategy reduces the time needed to load an application by postponing expensive symbol resolution. Benchmarks on Linux systems show startup times for large applications can be cut by 30–50% when using indirect symbols. However, the first call to a lazily resolved function incurs a small overhead (typically a few CPU cycles) due to the PLT stub and the loader invocation. In performance‑critical sections, developers may force eager binding by explicitly referencing functions or using compiler attributes such as attribute((used)).

Debugging and Symbol Information

Debugging tools such as gdb, lldb, and windbg rely on symbol tables to present source code and variable names to developers. Indirect symbols are displayed in the symbol list but often annotated as “lazy” or “imported.” During debugging, stepping into a lazy symbol triggers the dynamic loader, which can break on the loader’s breakpoint. This feature is useful for diagnosing library loading failures or symbol resolution errors. Additionally, static analysis tools can report missing indirect symbols to help developers detect unresolved dependencies early.

Cross‑Language Interoperability

Languages that compile to native code, such as Rust, Go, and Swift, often interoperate with C libraries. When binding to a C library, the compiler generates indirect references for functions that may be implemented in another language or loaded dynamically. For example, Go’s cgo tool generates indirect symbols for imported C functions, ensuring that the Go runtime can call them through the dynamic linker. Similarly, Rust’s foreign function interface (FFI) uses indirect symbols for C functions imported via the #[link(name = "...")] attribute.

Tools and Utilities

ld and binutils

The GNU linker (ld) is responsible for generating the relocation entries and PLT/GOT structures that implement indirect symbols. The --enable-new-dtags and --build-id options control how the dynamic section is populated. The binutils package also includes readelf and objdump, which can display information about indirect symbols, relocations, and PLT entries. Users can inspect lazy bindings with the -r flag to view unresolved relocations.

objdump, readelf, and dumpbin

objdump provides a -r option to list relocation entries, highlighting those that refer to lazy symbols. readelf’s -r and -s options allow users to inspect the dynamic symbol table and the associated relocation types. On Windows, the dumpbin utility (part of Visual Studio) can show the import table entries and IAT. These tools are indispensable for verifying that indirect symbols are correctly generated and for diagnosing loader issues.

Static Analysis and Format Checking

Static analyzers such as cppcheck, clang-tidy, and scan-build can be configured to check for unresolved indirect symbols. They parse the symbol tables and relocations, reporting missing or mismatched symbol names. Format checkers like elfcheck (from the elfutils package) validate that relocation entries are consistent with the dynamic symbol table. For PE/COFF, tools such as dumpbin and the Windows Platform SDK utilities can check the import directory for consistency.

Future Directions

Enhanced Lazy Binding

Research in dynamic loading is exploring advanced lazy binding techniques that combine Just‑In‑Time (JIT) compilation with indirect symbols. Projects like LLVM’s linker and runtime implement lazy binding with more fine‑grained control, allowing the JIT to generate optimized trampolines on the fly. Additionally, the Linux kernel’s systemd integration and the use of the LD_PRELOAD environment variable allow users to override indirect symbols at process start, providing a flexible sandboxing mechanism.

Security Implications

Indirect symbols can be a vector for attacks such as DLL hijacking or GOT overwrite exploits. The dynamic loader’s resolution process can be manipulated if an attacker can supply a malicious shared library that exports a symbol with the same name. Mitigation techniques include using --no-allow-shlib-undefined and the DT_BIND_NOW flag to force eager binding and reduce the attack surface. Additionally, modern kernels enforce read‑only and no‑execute permissions on the GOT to prevent memory corruption attacks.

References & Further Reading

Sources

The following sources were referenced in the creation of this article. Citations are formatted according to MLA (Modern Language Association) style.

  1. 1.
    "LLVM’s linker and runtime." github.com, https://github.com/llvm/llvm-project. Accessed 16 Apr. 2026.
  2. 2.
    "https://learn.microsoft.com/en-us/windows/win32/debug/pe-format." learn.microsoft.com, https://learn.microsoft.com/en-us/windows/win32/debug/pe-format. Accessed 16 Apr. 2026.
  3. 3.
    "https://sourceware.org/binutils/docs/ld." sourceware.org, https://sourceware.org/binutils/docs/ld. Accessed 16 Apr. 2026.
Was this helpful?

Share this article

See Also

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!