- Create a
no-stdshared librarylibgreet.sowhich exposes some functions and variables. - Create a
no-stduser executable which dynamically links againstlibgreet.soand uses exposed functions and variables. - Create a dynamic linker
dynld.sowhich can prepare the execution environment, by mapping the shared library dependency and resolving all relocations.
In code blocks included in this page, the error checking code is omitted to purely focus on the functionality they are trying to show-case.
To challenge the dynamic linker at least a little bit, the shared library will contain different functionality to generate different kinds of relocations.
The first part consists of a global variable gCalled and a global function
get_greet. Since the global variable is referenced in the function and the
variable does not have internal linkage, this will generate a relocation in
the shared library object.
int gCalled = 0;
const char* get_greet() {
// Reference global variable -> generates RELA relocation (R_X86_64_GLOB_DAT).
++gCalled;
return "Hello from libgreet.so!";
}Additionally the shared library contains a constructor and destructor
function which will be added to the .init_array and .fini_array sections
accordingly. The dynamic linkers task is to run these function during
initialization and shutdown of the shared library.
// Definition of `static` function which is referenced from the `DT_INIT_ARRAY`
// dynamic section entry -> generates R_X86_64_RELATIVE relocation.
__attribute__((constructor)) static void libinit() {
pfmt("libgreet.so: libinit\n");
}
// Definition of `non static` function which is referenced from the
// `DT_FINI_ARRAY` dynamic section entry -> generates R_X86_64_64 relocation.
__attribute__((destructor)) void libfini() {
pfmt("libgreet.so: libfini\n");
}
constructor/destructorare function attributes and their definition is described in gcc common function attributes.
The generated relocations can be seen in the readelf output of the shared
library ELF file.
> readelf -r libgreet.so
Relocation section '.rela.dyn' at offset 0x3f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000003e88 000000000008 R_X86_64_RELATIVE 1064
000000003e90 000300000001 R_X86_64_64 000000000000107c libfini + 0
000000003ff8 000400000006 R_X86_64_GLOB_DAT 0000000000004020 gCalled + 0
Relocation section '.rela.plt' at offset 0x438 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000004018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 pfmt + 0Dumping the .dynamic section of the shared library, it can be see that there
are INIT_* / FINI_* entries. These are generated as result of the
constructor / destructor functions.
The dynamic linker can make use of those entries at runtime to locate the
.init_array / .fini_array sections and run the functions accordingly.
> readelf -d libgreet.so
Dynamic section at offset 0x2e98 contains 18 entries:
Tag Type Name/Value
0x0000000000000019 (INIT_ARRAY) 0x3e88
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3e90
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
-- snip --
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x438
0x0000000000000007 (RELA) 0x3f0
0x0000000000000008 (RELASZ) 72 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x0000000000000000 (NULL) 0x0The full source code of the shared library is available in libgreet.c.
The user program looks as follows, it will just make use of the libgreet.so
global variable and functions.
// API of `libgreet.so`.
extern const char* get_greet();
extern const char* get_greet2();
extern int gCalled;
void _start() {
pfmt("Running _start() @ %s\n", __FILE__);
// Call function from libgreet.so -> generates PLT relocations (R_X86_64_JUMP_SLOT).
pfmt("get_greet() -> %s\n", get_greet());
pfmt("get_greet2() -> %s\n", get_greet2());
// Reference global variable from libgreet.so -> generates RELA relocation (R_X86_64_COPY).
pfmt("libgreet.so called %d times\n", gCalled);
}Inspecting the relocations again with readelf it can be seen that they
contain entries for the referenced variable and functions of the shared
library.
> readelf -r main
Relocation section '.rela.dyn' at offset 0x478 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000404028 000300000005 R_X86_64_COPY 0000000000404028 gCalled + 0
Relocation section '.rela.plt' at offset 0x490 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 get_greet + 0
000000404020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 get_greet2 + 0The last important piece is to dynamically link the user program against
libgreet.so which will generate a DT_NEEDED entry in the .dynamic
section.
> readelf -r -d main
Dynamic section at offset 0x2ec0 contains 15 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libgreet.so]
-- snip ---
0x0000000000000000 (NULL) 0x0The full source code of the user program is available in main.c.
The dynamic linker developed here is kept simple and mainly used to explore the mechanics of dynamic linking. That said, it means that it is tailored specifically for the previously developed executable and won't support things as
- Multiple shared library dependencies.
- Dynamic symbol resolve during runtime (lazy bindings).
- Passing arguments to the user program.
- Thread locals storage (TLS).
However, with a little effort, this dynamic linker could easily be extend and generalized more.
Before diving into details, let's first define the high-level structure of
dynld.so:
- Decode initial process state from the stack(
SystemV ABIcontext). - Map the
libgreet.soshared library dependency. - Resolve all relocations of
libgreet.soandmain. - Run
INITfunctions oflibgreet.soandmain. - Transfer control to user program
main. - Run
FINIfunctions oflibgreet.soandmain.
When discussing the dynamic linkers functionality below, it is helpful to understand and keep the following links between the ELF structures in mind.
- From the
PHDRthe dynamic linker can find the.dynamicsection. - From the
.dynamicsection, the dynamic linker can find all information required for dynamic linking such as therelocation table,symbol tableand so on.
PHDR
AT_PHDR ----> +------------+
| ... |
| | .dynamic
| PT_DYNAMIC | ----> +-----------+
| | | DT_SYMTAB | ----> [ Symbol Table (.dynsym) ]
| ... | | DT_STRTAB | ----> [ String Table (.dynstr) ]
+------------+ | DT_RELA | ----> [ Relocation Table (.rela.dyn) ]
| DT_JMPREL | ----> [ Relocation Table (.rela.plt) ]
| DT_NEEDED | ----> Shared Library Dependency
| ... |
+-----------+
This step consists of decoding the SystemV ABI block on the stack into an
appropriate data structure. The details about this have already been discussed
in 02 Process initialization.
typedef struct {
uint64_t argc; // Number of commandline arguments.
const char** argv; // List of pointer to command line arguments.
uint64_t envc; // Number of environment variables.
const char** envv; // List of pointers to environment variables.
uint64_t auxv[AT_MAX_CNT]; // Auxiliary vector entries.
} SystemVDescriptor;
void dl_entry(const uint64_t* prctx) {
// Parse SystemV ABI block.
const SystemVDescriptor sysv_desc = get_systemv_descriptor(prctx);
...With the SystemV ABI descriptor, the next step is to extract the information of
the user program that are of interest to the dynamic linker.
That information is captured in a dynamic shared object (dso) structure as
defined below.
typedef struct {
uint8_t* base; // Base address.
void (*entry)(); // Entry function.
uint64_t dynamic[DT_MAX_CNT]; // `.dynamic` section entries.
uint64_t needed[MAX_NEEDED]; // Shared object dependencies (`DT_NEEDED` entries).
uint32_t needed_len; // Number of `DT_NEEDED` entries (SO dependencies).
} Dso;Filling in the dso structure is achieved by following the ELF structures as
shown above.
First, the address of the program headers can be found in the AT_PHDR entry
in the auxiliary vector. From there the .dynamic section can be located by
using the program header PT_DYNAMIC->vaddr entry.
However before using the vaddr field, first the base address of the dso
needs to be computed. This is important because addresses in the program header
and the dynamic section are relative to the base address.
The base address can be computed by using the PT_PHDR program header which
describes the program headers itself. The absolute base address is then
computed by subtracting the relative PT_PHDR->vaddr from the absolute address
in the AT_PDHR entry from the auxiliary vector. Looking at the figure below
this becomes more clear.
VMA
| |
base address -> | | -
| | | <---------------------+
AT_PHDR -> +---------+ - |
| | |
| PT_PHDR | -----> Elf64Phdr { .., vaddr, .. }
| |
+---------+
| |
For
non-pieexecutables thebase addressis typically0x0, while forpieexecutables it is typically not0x0.
Looking at the concrete implementation in the dynamic linker, computing the
base address is done while iterating over the program headers. The result is
stored in the dso object representing the user program.
static Dso get_prog_dso(const SystemVDescriptor* sysv) {
...
const Elf64Phdr* phdr = (const Elf64Phdr*)sysv->auxv[AT_PHDR];
for (unsigned phdrnum = sysv->auxv[AT_PHNUM]; --phdrnum; ++phdr) {
if (phdr->type == PT_PHDR) {
prog.base = (uint8_t*)(sysv->auxv[AT_PHDR] - phdr->vaddr);
} else if (phdr->type == PT_DYNAMIC) {
dynoff = phdr->vaddr;
}
}Continuing, the next step is to decode the .dynamic section. Entries in the
.dynamic section are comprised of 2 x 64bit words and are interpreted as
follows:
typedef struct {
uint64_t tag;
union {
uint64_t val;
void* ptr;
};
} Elf64Dyn;Available
tagsare defined in elf.h.
The .dynamic section is located by using the offset from the
PT_DYNAMIC->vaddr entry and adding it to the absolute base address of the
dso. When iterating over the program headers above, this offset was already
stored in dynoff and passed to the decode_dynamic function.
static void decode_dynamic(Dso* dso, uint64_t dynoff) {
for (const Elf64Dyn* dyn = (const Elf64Dyn*)(dso->base + dynoff); dyn->tag != DT_NULL; ++dyn) {
if (dyn->tag == DT_NEEDED) {
dso->needed[dso->needed_len++] = dyn->val;
} else if (dyn->tag < DT_MAX_CNT) {
dso->dynamic[dyn->tag] = dyn->val;
}
}
...The value of
DT_NEEDEDentries contain indexes into thestring table (DR_STRTAB)to get the name of the share library dependency.
The last step to extract the information of the user program is to store the
address of the entry function where the dynamic linker will pass control to
once the execution environment is set up.
The address of the entry function can be retrieved from the AT_ENTRY entry
in the auxiliary vector.
static Dso get_prog_dso(const SystemVDescriptor* sysv) {
...
prog.entry = (void (*)())sysv->auxv[AT_ENTRY];The next step of the dynamic linker is to map the shared library dependency of
the main program. Therefore the value of the DT_NEEDED entry in the
.dynamic section is used. This entry holds an index into the string table
where the name of the dependency can be retrieved from.
static const char* get_str(const Dso* dso, uint64_t idx) {
return (const char*)(dso->base + dso->dynamic[DT_STRTAB] + idx);
}
void dl_entry(const uint64_t* prctx) {
...
const Dso dso_lib = map_dependency(get_str(&dso_prog, dso_prog.needed[0]));In this concrete case the main program only has a single shared library dependency. However ELF files can have multiple dependencies, in that case the
.dynamicsection contains multipleDT_NEEDEDentries.
The task of the map_dependency function now is to iterate over the program
headers of the shared library and map the segments described by each PT_LOAD
entry from file system into the virtual address space of the process.
To find the program headers, the first step is to read in the ELF header because this header contains the file offset and the number of program headers. This information is then used to read in the program headers from the file.
typedef struct {
uint64_t phoff; // Program header file offset.
uint16_t phnum; // Number of program header entries.
...
} Elf64Ehdr;
static Dso map_dependency(const char* dependency) {
const int fd = open(dependency, O_RDONLY);
// Read ELF header.
Elf64Ehdr ehdr;
read(fd, &ehdr, sizeof(ehdr);
// Read Program headers at offset `phoff`.
Elf64Phdr phdr[ehdr.phnum];
pread(fd, &phdr, sizeof(phdr), ehdr.phoff);
...Full definition of the
Elf64EhdrandElf64Phdrstructures are available in elf.h.
With the program headers available, the different PT_LOAD segments can be
mapped. The strategy here is to first map a whole region in the virtual address
space, big enough to hold all the PT_LOAD segments. Once the allocation
succeeded the single PT_LOAD segments can be mapped over the allocated
region.
To compute the length of the initial allocation, the start and end address
must be computed by iterating over all PT_LOAD entries and saving the minimal
and maximal address.
After that, the memory region is mmaped as private & anonymous mapping with
address == 0, telling the OS to choose a virtual address, and PROT_NONE
as the PT_LOAD segments define their own protection flags.
static Dso map_dependency(const char* dependency) {
...
// Compute start and end address.
uint64_t addr_start = (uint64_t)-1;
uint64_t addr_end = 0;
for (unsigned i = 0; i < ehdr.phnum; ++i) {
const Elf64Phdr* p = &phdr[i];
if (p->type == PT_LOAD) {
if (p->vaddr < addr_start) {
addr_start = p->vaddr;
} else if (p->vaddr + p->memsz > addr_end) {
addr_end = p->vaddr + p->memsz;
}
}
}
// Page align addresses.
addr_start = addr_start & ~(PAGE_SIZE - 1);
addr_end = (addr_end + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
// Allocate region big enough to fit all `PT_LOAD` sections.
uint8_t* map = mmap(0 /* addr */, addr_end - addr_start /* len */,
PROT_NONE /* prot */, MAP_PRIVATE | MAP_ANONYMOUS /* flags */,
-1 /* fd */, 0 /* file offset */);Now the single PT_LOAD segments can be mapped from the ELF file of the shared
library using the open file descriptor fd from above.
A segment could contain ELF sections of type SHT_NOBITS which contributes to
the segments memory image but don't contain actual data in the ELF file on disk
(typical for .bss the zero initialized section). Those sections are normally
at the end of the segment making the PT_LOAD->memzsz > PT_LOAD->filesz and
are initialized to 0 during runtime.
static Dso map_dependency(const char* dependency) {
...
// Compute base address for library.
uint8_t* base = map - addr_start;
for (unsigned i = 0; i < ehdr.phnum; ++i) {
const Elf64Phdr* p = &phdr[i];
if (p->type != PT_LOAD) {
continue;
}
// Page align addresses.
uint64_t addr_start = p->vaddr & ~(PAGE_SIZE - 1);
uint64_t addr_end = (p->vaddr + p->memsz + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
uint64_t off = p->offset & ~(PAGE_SIZE - 1);
// Compute segment permissions.
uint32_t prot = (p->flags & PF_X ? PROT_EXEC : 0) |
(p->flags & PF_R ? PROT_READ : 0) |
(p->flags & PF_W ? PROT_WRITE : 0);
// Mmap single `PT_LOAD` segment.
mmap(base + addr_start, addr_end - addr_start, prot, MAP_PRIVATE | MAP_FIXED, fd, off);
// Initialize trailing length (no allocated in ELF file).
if (p->memsz > p->filesz) {
memset(base + p->vaddr + p->filesz, 0 /* byte */, p->memsz - p->filesz /*len*/);
}
}With that the shared library dependency is mapped in to the virtual address
space of the user program. The last step is to decode the .dynamic section
and initialize the dso structure. This is the same as already done for the
user program above and details can be seen in the implementation in
map_dependency - dynld.c.
After mapping the shared library the next step is to resolve relocations.
This is the process of resolving references to symbols to actual addresses. For
shared libraries this must be done at runtime rather than static link time as
the base address of a shared library is only known at runtime.
One central structure for resolving relocations is the LinkMap. This is a
linked list of dso objects which defines the order in which dso objects are
used when performing symbol lookup.
typedef struct LinkMap {
const Dso* dso; // Pointer to Dso list object.
const struct LinkMap* next; // Pointer to next LinkMap entry ('0' terminates the list).
} LinkMap;In this implementation the LinkMap is setup as follows main -> libgreet.so,
meaning that symbols are first looked up in main and only if they are not
found, libgreet.so will be searched.
void dl_entry(const uint64_t* prctx) {
...
const LinkMap map_lib = {.dso = &dso_lib, .next = 0};
const LinkMap map_prog = {.dso = &dso_prog, .next = &map_lib};With the LinkMap setup the dynld.so can start processing relocations of the
main program and the shared library. The dynamic linker will process the
following two relocation tables for all dso objects on startup:
DT_RELA: Relocations that must be resolved during startup.DT_JMPREL: Relocations associated with the procedure linkage table (those could be resolved lazily during runtime, but here they are directly resolved during startup).
static void resolve_relocs(const Dso* dso, const LinkMap* map) {
for (unsigned long relocidx = 0; relocidx < (dso->dynamic[DT_RELASZ] / sizeof(Elf64Rela)); ++relocidx) {
const Elf64Rela* reloc = get_reloca(dso, relocidx);
resolve_reloc(dso, map, reloc);
}
for (unsigned long relocidx = 0; relocidx < (dso->dynamic[DT_PLTRELSZ] / sizeof(Elf64Rela)); ++relocidx) {
const Elf64Rela* reloc = get_pltreloca(dso, relocidx);
resolve_reloc(dso, map, reloc);
}
}The x86_64 SystemV ABI states that x86_64 only uses RELA relocation entries,
which are defined as:
typedef struct {
uint64_t offset; // Virtual address of the storage unit affected by the relocation.
uint64_t info; // Symbol table index + relocation type.
int64_t addend; // Constant value used to compute the relocation value.
} Elf64Rela;So each relocation entry provides the following information required to perform the relocation
- Virtual address of the storage unit that is affected by the relocation. This
is the address in memory where the actual address of the resolved symbol will
be stored to. It is encoded in the
Elf64Rela->offsetfield. - The symbol that needs to be looked up to resolve the relocation. The
upper 32 bit of the
Elf64Rela->infoencode the index into the symbol table. - The relocation type which describes how the relocation should be performed in
detail. It is encoded in the lower 32 bit of the
Elf64Rela->infofield.
The x86_64 SystemV ABI defines many relocation types. As an example, the
following two sub-sections will discuss the relocation types
R_X86_64_JUMP_SLOT and R_X86_64_COPY.
Relocation of type R_X86_64_JUMP_SLOT are used for entries related to the
procedure linkage table (PLT) which is used for function calls between dso
objects. This can be seen here, as the main program calls for example the
get_greet function provided by the libgreet.so shared library which creates
such a relocation entry.
> readelf -r main libgreet.so
...
Relocation section '.rela.plt' at offset 0x490 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 get_greet + 0
000000404020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 get_greet2 + 0To resolve relocations of this type the following steps need to be performed:
- Extract the name of the symbol from the relocation entry.
- Find the address of the symbol by walking the
LinkMapand searching for the symbol. - Patch the affected address of the relocation entry with the address of the symbol.
The code block below shows a simplified version of the resolve_reloc function
which only shows lines that are important for handling relocations of type.
R_X86_64_JUMP_SLOT.
static void resolve_reloc(const Dso* dso, const LinkMap* map, const Elf64Rela* reloc) {
// Get symbol information.
const int symidx = ELF64_R_SYM(reloc->info);
const Elf64Sym* sym = get_sym(dso, symidx);
const char* symname = get_str(dso, sym->name);
// Get relocation type.
const unsigned reloctype = ELF64_R_TYPE(reloc->info);
// assume reloctype == R_X86_64_JUMP_SLOT
// Lookup address of symbol.
void* symaddr = 0;
for (const LinkMap* lmap = map->next; lmap && symaddr == 0; lmap = lmap->next) {
symaddr = lookup_sym(lmap->dso, symname);
}
// Patch address affected by the relocation.
*(uint64_t*)(dso->base + reloc->offset) = (uint64_t)symaddr;
}The full implementation of the
resolve_relocfunction can be reviewed in resolve_reloc - dynld.c.
Relocations of type R_X86_64_COPY are used in the main program when referring
to an external object provided by a shared library, as for example a global
variable. Here the main program makes use the global variable extern int gCalled; defined in the libgreet.so which creates relocations as shown
in the readelf dump below.
> readelf -r main libgreet.so
File: main
Relocation section '.rela.dyn' at offset 0x478 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000404028 000300000005 R_X86_64_COPY 0000000000404028 gCalled + 0
...
File: libgreet.so
Relocation section '.rela.dyn' at offset 0x3f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
...
000000003ff8 000400000006 R_X86_64_GLOB_DAT 0000000000004020 gCalled + 0
...For relocations of this type, the static linker allocates space for the
external symbol in the main programs .bss sections.
> objdump -M intel -d -j .bss main
main: file format elf64-x86-64
Disassembly of section .bss:
0000000000404028 <gCalled>:
404028: 00 00 00 00Any reference to the symbol from within the main program is directly resolved
during static link time into the .bss section.
> objdump -M intel -d main
main: file format elf64-x86-64
Disassembly of section .text:
0000000000401030 <_start>:
...
401088: 8b 05 9a 2f 00 00 mov eax,DWORD PTR [rip+0x2f9a] # 404028 <gCalled>
...The R_X86_64_COPY relocation instructs the dynamic linker now to copy the
initial value from the shared library that provides it into the allocated space
in the main programs .bss section.
Shared libraries on the other hand that also reference the same symbol will go
though a GOT entry that is patched by the dynamic linker to point to the
location in the .bss section of the main program.
Below this can be seen by the mov instruction at address 1024 that the
relative address 3ff8 is dereferenced, which is the GOT entry for gCalled,
to get the address of gCalled. The next instruction at 102b then loads the
value of gCalled iteself. In the readelf dump above it can be seen that
there is a relocation of type R_X86_64_GLOB_DAT for symbol gCalled
affecting the relative address 3ff8 in the shared library.
> objdump -M intel -d -j .text -j .got libgreet.so
libgreet.so: file format elf64-x86-64
Disassembly of section .text:
0000000000001020 <get_greet>:
1020: 55 push rbp
1021: 48 89 e5 mov rbp,rsp
1024: 48 8b 05 cd 2f 00 00 mov rax,QWORD PTR [rip+0x2fcd] # 3ff8 <gCalled-0x28>
102b: 8b 00 mov eax,DWORD PTR [rax] # load gCalled
...
Disassembly of section .got:
0000000000003ff8 <.got>:
...The following figure visualizes the described layout above in some more detail.
libso
+-----------+
| .text |
main prog | | ref
+-----------+ | ... [foo] |--+
| .text | R_X86_64_GLOB_DAT | | |
ref | | Patch address of +-----------+ |
+--| ... [foo] | foo in .got. | .got | |
| | | +------------------>| foo: |<-+
| +-----------+ | | |
| | .bss | | +-----------+
| | | / | .data |
+->| foo: ... |<--------------------| foo: ... |
| | R_X86_64_COPY | |
+-----------+ Copy initial value. +-----------+
To resolve relocations of type R_X86_64_COPY the following steps need to be
performed:
- Extract the name of the symbol from the relocation entry.
- Find the address of the symbol by walking the
LinkMapand searching for the symbol and excluding the symbol table of the main programdso. - Copy over the initial value of the symbol into the affected address of the
relocation entry (
.bsssection of the main program).
The code block below shows a simplified version of the resolve_reloc function
which only shows lines that are important for handling relocations of type.
static void resolve_reloc(const Dso* dso, const LinkMap* map, const Elf64Rela* reloc) {
// Get symbol information.
const int symidx = ELF64_R_SYM(reloc->info);
const Elf64Sym* sym = get_sym(dso, symidx);
const char* symname = get_str(dso, sym->name);
// Get relocation type.
const unsigned reloctype = ELF64_R_TYPE(reloc->info);
// assume reloctype == R_X86_64_COPY
// Lookup address of symbol.
void* symaddr = 0;
for (const LinkMap* lmap = (reloctype == R_X86_64_COPY ? map->next : map); lmap && symaddr == 0; lmap = lmap->next) {
symaddr = lookup_sym(lmap->dso, symname);
}
// Copy initial value of variable into address affected by the relocation.
memcpy(dso->base + reloc->offset, (void*)symaddr, sym->size);
}The full implementation of the
resolve_relocfunction can be reviewed in resolve_reloc - dynld.c.
The next step before transferring control to the main program is to run all the
init functions for the dso objects. Examples for those are global
constructors.
typedef void (*initfptr)();
static void init(const Dso* dso) {
if (dso->dynamic[DT_INIT]) {
initfptr* fn = (initfptr*)(dso->base + dso->dynamic[DT_INIT]);
(*fn)();
}
size_t nfns = dso->dynamic[DT_INIT_ARRAYSZ] / sizeof(initfptr);
initfptr* fns = (initfptr*)(dso->base + dso->dynamic[DT_INIT_ARRAY]);
while (nfns--) {
(*fns++)();
}
}
void dl_entry(const uint64_t* prctx) {
...
// Initialize library.
init(&dso_lib);
// Initialize main program.
init(&dso_prog);
...
}At that point the execution environment is setup and control can be transferred from the dynamic linker to the main program.
void dl_entry(const uint64_t* prctx) {
...
// Transfer control to user program.
dso_prog.entry();
...
}After the main program returned and before terminating the process all the
fini functions for the dso objects are executed. Examples for those are
global destructors.
typedef void (*finifptr)();
static void fini(const Dso* dso) {
size_t nfns = dso->dynamic[DT_FINI_ARRAYSZ] / sizeof(finifptr);
finifptr* fns = (finifptr*)(dso->base + dso->dynamic[DT_FINI_ARRAY]) + nfns /* reverse destruction order */;
while (nfns--) {
(*--fns)();
}
if (dso->dynamic[DT_FINI]) {
finifptr* fn = (finifptr*)(dso->base + dso->dynamic[DT_FINI]);
(*fn)();
}
}
void dl_entry(const uint64_t* prctx) {
...
// Finalize main program.
fini(&dso_prog);
// Finalize library.
fini(&dso_lib);
...
}