An educational implementation of a dynamic linker for Linux x86_64, designed to demonstrate how dynamic linking works under the hood.
This project implements a minimal dynamic linker that can:
- Load and parse ELF executables and shared libraries
- Resolve symbols between libraries
- Apply relocations (GOT, PLT, RELA)
- Execute library initialization functions
- Transfer control to the main program
The implementation is educational and focuses on clarity over performance or completeness.
src/toy_linker.c
- Main dynamic linker entry point and orchestrationsrc/auxv.h
- Auxiliary vector parsing (kernel-provided information)src/program_header.h
- ELF program header parsingsrc/dynamic_section.h
- Dynamic section parsing (DT_* entries)src/loader.h
- Library loading and memory mappingsrc/symbol.h
- Symbol table management and lookupsrc/hash_table.h
- Hash table for fast symbol resolutionsrc/relocation.h
- Relocation processing (GOT/PLT patching)src/init_libs.h
- Library initialization function executionsrc/util.h
- System calls and utility functions (nostdlib environment)
- No Standard Library: Uses direct system calls to avoid circular dependencies with glibc
- Header-Only Implementation: All functions are
static inline
to avoid PLT issues - since the dynamic linker is responsible for resolving PLT entries, it cannot have its own unresolved PLT entries! The dynamic linker must be self-contained with no external function calls to resolve. - Educational Focus: Extensive debug output and clear code structure to show each step
- Safety First: Validates pointers and handles errors gracefully - the dynamic linker runs before main program error handling is available
# Create build directory
mkdir -p build
cd build
# Configure with CMake
cmake -G Ninja ..
# Build
ninja
The project includes a simple example program that demonstrates the dynamic linker functionality:
example/simple_program.c
- Main program with constructor functionexample/simple_lib.c
- Shared library with constructor and utility functions
# After building, run from the build directory
cd build/bin
./simple_program
=== Toy Dynamic Linker Debug Output ===
[Extensive debug information about loading, symbol resolution, relocations...]
=== Executing All Initialization Functions ===
Skipping init functions for dynamic linker itself
Executing init functions for main_executable
Calling DT_INIT_ARRAY (1 functions)
Function 0 at 401030
[PROGRAM INIT] simple_program constructor called!
Executing init functions for libsimple_lib.so
Calling DT_INIT_ARRAY (1 functions)
Function 0 at 7f7ef21f3030
[LIB INIT] simple_lib constructor called!
=== Initialization Complete ===
Hello from dynamically linked program!
This message is printed using the shared library function!
The example demonstrates __attribute__((constructor))
functions:
__attribute__((constructor))
void program_init(void) {
// Called during program initialization
}
Shows how shared library functions are resolved and called:
// In simple_lib.c
int simple_strlen(const char *str);
void print_string(const char *message);
// Called from simple_program.c
int len = simple_strlen("Hello World");
print_string("Hello from library!");
Demonstrates how symbols are resolved between the main program and shared libraries through GOT/PLT mechanisms.
Shows the proper order of initialization:
- System libraries (skipped for safety)
- User shared libraries
- Main executable
This is an educational toy implementation with several limitations:
- TLS (Thread Local Storage) - No support for
__thread
variables - C++ Support - No C++ runtime initialization
- System Library Init - Skips glibc initialization (requires full C runtime)
- Advanced Relocations - Only basic RELA, GOT, and PLT relocations
- Security Features - No RELRO, stack canaries, or other hardening
- Performance Optimizations - Focus is on clarity, not speed
- TLS Complexity: Requires kernel support and complex setup
- System Libraries: glibc initialization needs full C runtime environment
- Educational Focus: Implementing everything would obscure the core concepts
- Kernel loads the program and sets our toy linker as the interpreter
- Kernel provides auxiliary vector with program information
- Our
_start()
function receives control with the original stack
- Parse auxiliary vector - Get program headers, entry point, etc.
- Parse program headers - Find PT_DYNAMIC segment
- Parse dynamic section - Extract needed libraries, symbol tables, etc.
- Load dependencies - Map shared libraries into memory
- Build symbol table - Create hash table for fast symbol lookup
- Apply relocations - Patch GOT/PLT entries with resolved addresses
- Execute initialization - Call constructor functions
- Transfer control - Jump to main program entry point
- Uses custom
malloc_custom()
implementation based onmmap()
- Cleans up all memory before transferring control
- No memory leaks in the dynamic linker itself
The toy linker provides extensive debug output showing:
- Auxiliary vector contents
- Program header information
- Dynamic section entries
- Library loading process
- Symbol resolution details
- Relocation application
- Initialization function calls
- Memory management operations
This makes it excellent for understanding how dynamic linking works step by step.
This project is for educational purposes. Feel free to use and modify for learning about dynamic linking.