Experiment and learn how compilers, linkers and C runtime work cooperatively
For a fully-statically linked PIE or non-PIE binary, the CRT is responsible for three main tasks in order:
In general, the CRT has to apply two types of relocations:
Right after applying the dynamic and ifunc relocations, the CRT has to initialize the TLS for the main thread. The TLS layout depends on the thread model adopted by the libc. Typically, libc implementations support the POSIX pthreads based model and provide the stdlib threads.h API over it.
The CRT has to perform two types of initialization and finalization.
Call the _init function - A CRT typically only supplies the prologue and
epilogue for this function and places them in a .init section. On most
architectures, including x86_64, there is nothing much more than this. On a
few embedded architectures however, the compilers emit global variable
constructors (direct or indirect) into the .init section. This way, they
become part of the _init function and get invoked when the CRT calls the
_init function. In the case of glibc, one can define a special macro called
PREINIT_FUNCTION at glibc build time. This function gets called before
anything else in the _init function.
Call the callbacks in .init_array - The contents of .init_array section
are filled in by the compiler. If the compiler does not put the callbacks for
global variable constructors in the .init section, then it will put them in
the .init_array section. For example, this is done in the case of x86_64.
The CRT invokes these callbacks after it calls the _init function.
Call the _fini function
Call the callbacks in the .fini_array