To answer your question, let us first set a solid ground in terms of entities and definitions.
ELF stands for "Executable and Linkable format".
That is, it defines the structure and shape of two types of files:
- Executables (Shared Objects *.so and stand-alone executables)
- Linkables (Object files *.o)
Let us focus on executables.
Dependencies resolution of executables
Among other things, ELF defines a method of describing and resolving dependencies of executable.
Dependencies
Put simple, dependencies are required external symbols.
Symbols are named (identified) chunks of memory.
Some of the chunks are data chunks (Global variables) while others are code-data chunks (Global functions).
Since a symbol is a part of a module (aka Shared Object), any required symbol is coupled with a module.
In summary, dependencies are needed symbols and modules.
Note that a function that is a part of an OS API could be and usually is an external symbol. However, it's not always the case.
Dependencies description
ELF defines a structure called Dynamic Segment used to store information needed by the loader (aka dynamic linker) in the loading process of an executable.
An executable's dependencies description is stored in its Dynamic Segment.
Needed symbols are organized in a table called Dynamic Symbol Table that's referred by the Dynamic Segment:
Reference to a symbol table under Loader directives- https://elfy.io/KYze4
A dynamic symbol table is a contiguous array of symbol descriptors:
.dynsym under Symbols - https://elfy.io/KYze4
Needed modules on the other hand are described directly with DT_NEEDED entries:
Needed modules under Loader directives - https://elfy.io/KYze4
Dynamic link
Now we are ready to discuss the wiring mechanism that lets an executable reach its dependencies once they are resolved by the loader.
We will do it by following the steps of an external function call.
Let's take a call to __android_log_print as an example (ARM 32 bit).
...
1d21a: f7fa e8e8 blx 173ec ; __android_log_print@plt
...
The above is an assembly that calls __android_log_print which prints out text to Android Logcat.
But in fact, that blx instruction branches to a specific code-stub in a special area called Procedure Link Table (PLT).
There's a code stub in the PLT for every needed external function.
Here's __android_log_print's stub:
...
000173ec __android_log_print@plt:
173ec: e28fc600 add ip, pc, #0, 12
173f0: e28cca11 add ip, ip, #69632
173f4: e5bcf9f4 ldr pc, [ip, #2548]!
000173f8 sleep@plt:
173f8: e28fc600 add ip, pc, #0, 12
173fc: e28cca11 add ip, ip, #69632
17400: e5bcf9ec ldr pc, [ip, #2540]!
...
The three instructions in the stub do the following: (pseudo code)
JUMP *(GOT_ADDRESS + GOT_OFFSET_OF(__android_log_print))
The Global Offset Table (GOT) is a table of pointers.
There's a cell in the GOT for every external function.
That is, every external function has its own cell in the GOT.
Once the loading process is done, the cell of function X contains the memory address of function X.
- The address computation of the right cell in the GOT is split into 3 because of
encoding limitations. eg: Large offsets can't be encoded in a
single instruction.
It's the OS loader responsibility to initialize the GOT with the right memory addresses, based on the information discussed before.
The PLT and GOT are parts of the ELF specification.