Deep Dive: Linker Architecture
The dynamic linker in Android (often referred to as linker or linker64) is responsible for loading native ELF executables and shared libraries (.so files) into memory, resolving their dependencies, and applying relocations.
ELF Loading Process
When an Android application starts, the kernel loads the executable (usually app_process) and maps the dynamic linker into the process space. The linker then takes over, following these steps:
- Parsing ELF Headers: The linker reads the ELF header and program headers to determine the layout of the segments (e.g.,
.text,.data,.bss). - Memory Mapping: Segments are mapped into the process's virtual memory using
mmap(). - Dependency Graph Resolution: The linker inspects the
DT_NEEDEDentries in thePT_DYNAMICsegment to find required shared libraries (likelibc.soorliblog.so). This process repeats recursively. - Relocation: Address references within the loaded objects are patched so that function calls and data accesses point to the correct memory addresses.
- Execution: The linker calls the initialization functions (e.g., constructors in
.init_array) and finally jumps to the entry point of the executable.
Symbol Resolution: Weak, Strong, and Versioned
During the loading process, the linker must resolve symbols (functions and variables):
- Strong Symbols: The default symbol type. If the linker finds multiple strong symbols with the same name, it generates a collision error.
- Weak Symbols: A fallback symbol. If a strong symbol is found, the weak symbol is overridden. If no strong symbol exists, the weak symbol is used.
- Versioned Symbols: Bionic supports symbol versioning to maintain backward compatibility. Libraries can export multiple versions of a single function, allowing older apps to link against legacy implementations while new apps use the updated ones.
Relocation Types
Relocation is the process of connecting symbolic references with actual memory addresses. Key relocation types include:
- Absolute Relocations: Provide the exact address. These are less common in modern position-independent executables (PIE).
- Relative Relocations (
R_AARCH64_RELATIVE): Used when a library is loaded at a randomized base address (ASLR). The linker simply adds the base address to the offset. - Global Offset Table (GOT) Relocations: Used for external function calls. The linker populates the GOT with the actual addresses of external functions, and the code jumps to the addresses stored in the GOT.
Android's linker uses a packed relocation format to significantly reduce the size of the .rel.dyn and .rela.plt sections, saving memory and storage.
GNU Hash vs SYSV Hash
To resolve symbols quickly, the linker uses hash tables stored in the ELF file:
- SYSV Hash (
DT_HASH): The traditional ELF hash table. It relies on a classic array-based bucket and chain approach. - GNU Hash (
DT_GNU_HASH): A more modern and highly optimized hash table format. It includes a Bloom filter that quickly rules out missing symbols without traversing chains, vastly speeding up symbol lookup.
Android's dynamic linker strongly prefers GNU hash for performance reasons and mandates it for system libraries.
# To view the dynamic section and hash tables of an Android library:
readelf -d /apex/com.android.runtime/lib64/bionic/libc.so