C compilation process
Assembly Code
Human readable code, that has 1:1 analogous to machine instructions. assembler is used convert assembly code to relocatable object code.
It is possible to have different syntax for assembly language. Eg. for x86, In original Intel syntax add eax,[ebx]
will add memory data to a register; whereas this would be written as addl (%ebx),%eax
in the AT&T syntax, both of them would map to the same machine instruction. The assembler decides which syntax to support, it can also support multiple syntax.
Machine code
Machine code is code that is directly executable by the computer’s physical processor without further translation. Load into memory and go. Languages like C generate machinecode in the end rather than something like bytecode.
Object Code
- These are binary files but not necessrily machine code which contain additional metadata that will enable a linker, loader to assemble it with other object code modules into executable machine code or bytecode.
- Relocatable object files(
.o
) and *shared object files(.so
)*(relocatable object files that can be linked dynamically) are generated by compiler or assembler. - Executable object files(
a.out
, usually no extension) are generated by Linker. These can be directly copied into memory and executed. - Can have various formats,
a.out=(historical), =PE
,Mach-O
,ELF
. ButELF
is super common and is a common formot for all the 3 types of object files mentioned.
Bytecode
Byte code is code that can be executed and understood by a virtual machine/runtime. The virtual machine implementation reads the bytecode and performs the operations it specifies within a virtual environment. Byte code is typically slower than machine code but is portable across platforms. Languages/runtime that use bytecode include Java, Python, Dalvik/ART, Lua, WebAssembly etc.
Eg. Bytecode is sort of the object code for the JVM. But, an object code isn’t necessarly bytecode.
GCC
This is about GCC but probably applies to CLANG too except all the cross compilation stuff.
The executable named gcc
is just a compiler driver/compiler system that selects the “real” compiler and invokes the other components of gcc when needed. It does many things like preprocessing(cpp), compiling(cc1/cc1plus/cc1obj), assembling(as) and linking(ld)
gcc frontends and backends
- You can write your own frontend/backend for gcc
- frontend is machine independent but language specific
- backend is language independent but machine specific
- For example, if you have a C++ front end and a Java front end, you can accept input in C++ and Java. If you have an x86 back end and a MIPS back end, you can produce executables for both x86 and MIPS CPUs.
- The compilers such as cc1/cc1plus are both the frontend and the backends, they are linked into one executable.
gcc Breakdown
Preprocessing
Preprocessor converts source code into ascii intermediate file. Also called the Translation unit
$ cpp main.c main.i
$ gcc -E main.c -o main.i # same as the previous command
Compile
Compiler converts ascii intermediate file into assembly file. In reality you don’t call cc1
, gcc
calls it for you.
$ cc1 main.i -o main.s
Assemble
Assembler translates assembly file to binary reloc. obj file
$ as -o main.o main.s
Linking
Linker combines other obj, shared obj and generates the exec obj.
- Following snippet won’t work because how the C standard library is linked. (Read C Runtime (CRT) for more info, it needs to link
crt0
) - We can run
gcc
with the-v
to see the linker commands thatgcc
runs. (Note: collect2 is alias forld
)
$ ld -o main main.o
$ ld -e main -dynamic-linker /usr/lib64/ld-linux-x86-64.so.2 main.o -lc
Executing/Loading
To execute things, we need a loader. In linux, this can be the execve
syscall.
# Loader executes the program
$ ./main # execute
$ strace ./main # inspect execution
execve("./main", ["./main"], 0x7fffbc8857b0 /* 55 vars */) = 0
...
C Runtime (CRT)
In the linking phase, we have two compilation flavours/envs to think about: Hosted and Freestanding. In both cases C uses crt0
. It is an object file that the linker links to every file that’s built.
Hosted
-fhosted
: C standard library is available.
-
Linker and crt0
- The linker(
ld
) controls the program’s entrypoint and in generalgcc
points the entrypoint to_start
crt0
defines the content of the_start
.- If the linker cannot not find the
_start
symbol, it’ll set the entrypoint to the beginning of the.text
segment.
- The linker(
-
Loader and crt0
crt0
is like a shim layer between the program loader and your program.- A program is loaded into memory by a
loader
(eg.execve
) which is responsible for setting up theELF segments
etc. - Once the segments are loaded, the crt0 (
.s
/.o
) can do its initialization stuff (populateargv
etc.)crt0
setup the C runtime environment and then callmain
(libc.so.6:__libc_start_main
then calls outmain
).main
function is an idea of the c language not relating to assembly language or the linker.
Freestanding
-fhosted
: C standard library is un-available, embedded systems maybe.- No
loader
available thecrt0
has more work to do, such as exception vector setup, initilizing the stack and frame pointers etc. - It’s also useful to supply your own
_start
when you want to write really tiny programs or programs that do unconventional things.
More about Loader
Even though loader is not related to
gcc
, it’s useful to know more about it.
Loader is a program in the operating system that takes a program from disk and loads it into memory. In linux world we can use execve() system call to invoke the program loader.
Responsibilities
- Check permissions, Allocate space for the program’s stack
- Allocate space for the program’s heap, Initialize registers (e.g., stack pointer)
- Push argc, argv, and envp onto the program stack, Map virtual address spaces
- Dynamic linking, Relocations, Call pre-initialization functions
What it loads
- Once things are initialized, call into the entrypoint which is generally
_start
- Linux loads the
.text
section into memory only once, no matter how many times an application is loaded - Reduces memory usage & launch time and is safe because the code doesn’t change. (
.rodata
is backed into samesegment
as.text
) .data
section contains information that could be changed during application execution, so this section must be copied for every instance.
Segment vs Section
- Loader loads the segments, not sections. (See elf)
- Loads the segment if it is of type
PT_LOAD
/LOAD
.
Background and Jargons
Flat Binary
Binary files are just sequence of bytes in a file instead of text files. They can be compiled computer programs aswell as images, videos etc. Usually binary files contain some header for metadata which can contain some magic number to identify the format of the binary file. Binaries that do not contain any header are called flat binaries.
Binary and Executables
When speaking casually we sometimes mix binaries and executables, they are not the same. Not all binaries are executables. eg. Object code are binaries but they are not executables, on the other hand a directory can be an executable. ELF can be used both for executables and non-executable binaries. To confuse this even more, some references use the terms /relocatable object file/(for object files) and /executable object file/(for executables obtained from relocatable object file).
About Linkers
The linker merges together all sections of the same type included in the input object files into a single section and assigns an initial address to it. For instance, the .text
sections of all object files are merged together into a single .text
section, which by default contains all of the code in the program.
Linker Types
Link Editor
Static Linker
- Scripts (LD)
- You can provide a custom script
- Link editor may provide a script for static linking
Dynamic Linker
- Drew on Dynamic Linking
- ld-linux.so(8): dynamic linker/loader - Linux man page
- It itself is statically linked
ldd $(which ld.so)
and works a bit like#!/bin/sh
in scripts.
-
How?
- It mmaps the executable files, and any shared libraries it needs, using the
MAP_PRIVATE
flag. - It can perform the dynamic linking fixups to pages allowing the executable’s calls out to the shared library. (Uses
mprotect()
) - Dynamic linker will ignore LD_PRELOAD if
ruid != euid
- It mmaps the executable files, and any shared libraries it needs, using the
Linker Objective
- Symbol Resolution
- Symbol Relocation
Symbols
When you write a program in any language above direct machine code, you give symbolic names to functions and data and the compiler turns these things into code. At the machine level, they are known only by their address (offset within the file) and their size. But we need some way to refer to these, Symbols
.
Symbols in the C compilation process
cc1
generates assembly file(.s
) which contains the symbols in text format, the compiler may do some mangling here(eg. remove unused vars, so they never make to the symbol table!)as
takes the.s
file and creates theobj
file.- It creates the symbol table in the
.symtab
section of the ELF object. - In ELF64,
.symtab
contains an array entries of structElf64_Sym
.
- It creates the symbol table in the
ld
(linker) will look into.symtab
,.rel.text
,.rel.data
to perform symbol resolution and relocation- Linker puts
sections
intosegments
- For dynamic executables, it’ll leave some of the linking to the loader/dynamic linker. (Partial Linking)
- Linker puts
- loader makes use of the
program header table
to determine what things to put in memory for creating process images etc.- For dynamic executables, the dynamic linker(
ld.so
/ld-linux.so
) uses lazy loading to resolve the symbols usingGOT
andPLT
- We could also use
-fno-plt
to resolve all external symbols at load time.
- For dynamic executables, the dynamic linker(
Symbol Table
- The assembler creates the symbol table in the
.symtab
section of the ELF object..o
file will contain thesections
.symtab
contains references and information about the symbols- Real value of the symbol can exists in different sections of the object file(
.o
) - Each symbol is stored as a
Elf64_Sym
structtypedef struct { Elf64_Word st_name; // byte offset into .strtab for string name of symbol unsigned char st_info; // type and binding attributes, useful for the link-editor unsigned char st_other; Elf64_Half st_shndx; // readelf -s Ndx column Elf64_Addr st_value; // address/offset(for reloc) of the section pointed by st_shndx Elf64_Xword st_size; ... } Elf64_Sym;
- Can use
readelf -s
to inspect the symbol table. (There might be pseudosections such asUNDEF
,COMMON
,ABS
etc.)
More on st-info
-
Types
STT_NOTYPE
,STT_OBJECT
,STT_FUNC
,STT_SECTION
,STT_FILE
,STT_COMMON
,STT_TLS
,STT_LOOS
,STT_HIOS
,STT_LOPROC
,STT_SPARC_REGISTER
,STT_HIPROC
See Undefined, Tentative(STT_COMMON
), Defined.
-
Bindings
STB_LOCAL
,STB_GLOBAL
,STB_WEAK
,STB_LOOS
,STB_HIOS
,STB_LOPROC
,STB_HIPROC
-
Strong and Weak Symbols
When multiple global variables are declared on object files, linker uses the idea of strong and weak symbols to choose which one to use.
int x; // weak global symbol - STB_WEAK int y=10; // strong global symbol - STB_GLOBAL
-
Global, Local and Extern
- Global Symbols : Defined by
m
which can be referenced by other modules.STB_GLOBAL
- External Symbols :
Global Symbols
that are referenced bym
, but by some other module.STB_GLOBAL
- Local Symbols : Defined and referenced exclusively by
m
,STB_LOCAL
static
non- static
Global Variable Local Symbol Global Symbol Local Variable Local Symbol w unique name(stored in .data
/.bss
)Maintained at runtime on the stack Global Function Local Symbol Global Symbol Local Function No such thing No such thing - Global Symbols : Defined by
-
Dynamic Symbol Table
Where?
.dynsym
is found inshared objects
anddynamic executables
but not found in regular relocatable object files.- The relocation operation with dynamic symbols also relies on two extra tables
.rela.dyn
: Relocation for dynamically linked objects (data or procedures), if PLT is not used..rela.plt
: List of elements in the PLT, which are liable to the relocation during the dynamic linking.
# some observations
$ gcc main.c # .dynsym .dynstr .rela.dyn .rela.plt
$ gcc -fno-plt main.c # .dynsym .dynstr .rela.dyn
$ gcc -static main.c # .rela.plt (?)
$ gcc -static -fno-plt main.c # .rela.plt (?)
# bonus: see md5 sums, or use the size command
What?
.dynsym
is a smaller version of.symtab
that only contains global dynamic symbols. (kept separate to ease the operation of relocation)- Information found in the
.dynsym
is therefore also found in the.symtab
, while the reverse is not necessarily true. - For storing null-terminated strings,
.dynstr
is used with.dynsym
. - The symbols in
.dynsym
are called dynamic linker symbols, well because the dynamic linker makes use of these. - Historical context
Symbol Resolution
Associate each symbol references with exactly one symbol definition.
void poop() {...} // define symbol swap
poop(); // reference symbol swap
int *xp = &x; // define symbol xp, reference x
Compile Time Symbol Resolution
- The compiler just looks at the ascii intermediate code and generates symbols in the assembly file.
- It does not create the symbol table yet.
- Sometimes compile-time is referenced for link-time aswell.
Link Time Symbol Resolution
- The assembler creates the symbol table in the
.symtab
section of the ELF object..o
file will contain thesections
..symtab
entries are also referred to aslinker symbols
- Static Linker uses the
st-info
andreloc.
info to make decisions to how to reference and relocate things.- Sometimes absolutely (linker scripts)
- Sometimes partially so that the dynamic linker(
ld.so
) can do rest of the linking.
Load Time Symbol Resolution (Dynamic Linking)
- During the linking phase, static linker creates the executable but does not link the shared libraries, But does make a note of which shared libraries to load so that the loader/runtime can load them as required.
- Now the loader calls the dynamic linker (
ld-linux.so
), it lazyloads the symbols from shared libraries
Run Time Symbol Resolution (Dynamic Linking)
One could resolve symbols while the program is running as-well with functions such as dlopen
.
Symbol Relocation
When assembler generates an obj module, it does not know where the code and data will ultimately be stored in memory. Nor does it know the location of any externally defined funcs or global vars that are referenced by the module. So it creates reminders to the linker called the “relocation entries” which are stored in .rel.data
and .rel.text
that the linker can later use.
Relocation
- Linker relocates symbols by associating a mem location with each symbol definition
- Modifying all the references to those symbols so that they point to this mem location.
Dynamic Relocation
- Dynamic relocation is done the dynamic linker
- Not only shared objects but also dynamic (non-static) executables may have dynamic relocations.
Libraries
- When writing programs we need some way to package commonly used functions, for this we need libraries.
- Loading libraries is essentially Symbol Resolution and Symbol Relocation
About the C standard library
- The files that we include using the
#include
directives are just header files that consists of defines and types - These are concatenated to the ascii intermediate file in the pre-processing phase itself.
- Eg.
#include<stdio.h>
The functions mentioned in this header file need to be implemented by the C standard library, which is later linked by the linker.
Naming Convention
lib<name>.a
for archive libraries (static)lib<name>.so
for shared libraries (dynamic)- Eg.
libc.so
,libm.a
etc. - This is not actually mandated, convention and based on how the linker does lookup
- The
.so
and.a
are actually linker scripts. The actual libraries lives in/usr/lib/libm.so.6
and/usr/lib/libm-2.37.a
$ ls /usr/lib/libm.*
/usr/lib/libm.a /usr/lib/libm.so /usr/lib/libm.so.6
$ cat /usr/lib/libm.a
/* GNU ld script
*/
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /usr/lib/libm-2.37.a /usr/lib/libmvec.a )
$ cat /usr/lib/libm.so
/* GNU ld script
*/
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /usr/lib/libm.so.6 AS_NEEDED ( /usr/lib/libmvec.so.1 ) )
$ cat /usr/lib/libm.so.6
# ┈┈┈┈▕▔╱▔▔▔━▁
# ┈┈┈▕▔╱╱╱👁┈╲▂▔▔╲
# ┈┈▕▔╱╱╱╱💧▂▂▂▂▂▂▏
# ┈▕▔╱▕▕╱╱╱┈▽▽▽▽▽
# ▕▔╱┊┈╲╲╲╲▂△△△△
# ▔╱┊┈╱▕╲▂▂▂▂▂▂╱
# ╱┊┈╱┉▕┉┋╲┈
Static Libraries
- These are the
.a
(archive) files or a collection of.o
files. - Basically concats the relocatable object files into one file with and index.
- These are not ELF files but rather archive files generated by the
ar
command.
Linking static libraries
- Linking against a static library is roughly the same as just adding more .o files to the linker line.
- If symbol is found to be in one of the reloc(
.o
) files from the index, it’ll link the entire objectfile to the executable. - Ordering of
-l
flag matters, best practice is to place-l
flags towards the end based on howld
does symbol lookup.
Static Libraries vs Static Executables
- In Unix, the default compilation mode for programs is to use the systems shared library, instead of pre-linking everything necessary in the executable.
- When compiling generally without any flags, if both static and shared libraries are found, the linker gives preference to linking with the shared library unless the
-static
option is used. - When compiling a program with
gcc
, for instance, you pass the-static
flag if you wish it to be a fully linked static executable, instead of having unresolved symbolic references. - If you use
-static
you won’t have the.dynsym
inreadelf -s
output and filesize will be significantly larger. - Be aware that statically linking glibc is strongly advised against by the glibc maintainers, and some features of glibc will not work when statically linked.
- You can see
-Bstatic
to link certain libraries statically.
Shared Libraries
Many Names
- shared libraries
- shared objects
- dynamic shared objects (DSOs)
- dynamically linked libraries(DLLs)
Issues
- Each program can use any number of shared libraries, and there’s simply no way to know in advance where any given shared library will be loaded in the process’s virtual memory. There are two well known ways to resolve this
- Load-time relocation : x86-64 no longer supports load time relocation for shared objects, it’ll still work on i386. This will cause the program linker to generate a lot of relocation information, and cause the dynamic linker to do a lot of processing at runtime. ❌
- Position Independent Code (PIC) ✅
- Most shared libraries are compiled as Position Independent Code (PIC)
More info on shared libs
- memory - Loading of shared libraries and RAM usage - Unix & Linux Stack Exchange
- How to Write Shared Libraries by Ulrich Drepper(ex glibc maintainer)
Usage of PIC, PIE, -shared
, -static
This confused me to hell and back when I first looked all these flags at the same time. nothing made sense, tbh the naming could have been better. So if you’re at my place and you see this section by luck, you’ll be saving up on 3 smokes total.
The following options are described in more details in gcc
man page.
Code Generation Options
-fpic
,-fPIC
: Generate pic code for object files. They are different?-fpie
,-fPIE
: Generate pie code for object files.-fno-pic
: No PIC, no PIE code. Not listed ingcc
’s man page as such though. One could usemd5sum
/size
/objdump
to see the differences in these three options.--no-pic
: I am not sure, did they throw away this flag or it never existed?
Options for linking
-pie
: Generate PIE executable,ET_DYN
-no-pie
: Generate non-PIE executable,ET_EXEC
-static-pie
: Generate static PIE executable, this has many quirks, just look them up.ET_DYN
-static
: Generate static non-PIE executable and overrides-pie
, has nothing to do with generating static libraries, we also see a missing.dynamic
section when using this option.ET_EXEC
-shared
: Used when generating shared libraries.-rdynamic
: TODO
Conclusion about this mess
- Most distos nowadays have gcc configured with
--enable-default-pie
which makes gcc use-fPIE
and-pie
by default. - We need both
-fPIE
and-pie
because you see, they do different things. If we just use-pie
and it should fail because then the generated code will be using absolute addressing which is not compitable with PIE executables. So the following fails:$ gcc -fno-pic -pie main.c /usr/bin/ld: /tmp/ccTOkUR5.o: relocation R_X86_64_32 against `.rodata' can not be used when making a PIE object; recompile with -fPIE collect2: error: ld returned 1 exit status
Env & Setting up of Libraries
LDFLAGS
,CFLAGS
: These are a convention that comes from the predefined rules for MakefilesLD_LIBRARY_PATH
: A colon-separated set of directories where libraries should be searched for first, before the standard set of directories. Handy for development and testing, but shouldn’t be modified by an installation process for normal use by normal usersLD_PRELOAD
: It lists shared libraries with functions that override the standard set, just like/etc/ld.so.preload
.LD_DEBUG
: Triggers thedl*
functions so that they give verbose information, can have different values.ldconfig
: It reads the content of/etc/ld.so.conf
, creates the appropriate symbolic links in the dynamic link directories, and then writes a cache to/etc/ld.so.cache
which is then easily used by other programs. We need to run this everytime we updateld.so.conf
/etc/ld.so.preload
: Serves the same purpose as setting theLD_PRELOAD
variable, i.e. as a means to temporarily linking in a different library than usual.- When using shared libraries, we need to use the shared library both when compiling and when running our program. When compiling we use the combination of the
-L
and-l
flag to specify the shared library so that the linker is able to find the symbols. For running we need to updateld.so.conf
and runldconfig
so that when running our program, the dynamic linker is able to find the shared libraries and hence the symbols we’re using. rpath
: TODO
PIC, ASLR, PIE, GOT and PLT
Amount of misinformation in the following section CAN BE MASSIVE. So take everything w a big grain of salt.
These things are related, it’s interesting to know how and it is related how addressing works.
PIC (Position Independent Code)
- The idea behind PIC is simple, add an additional level of indirection to all global data and function references in the code.
- It’s an old concept that we needed when we needed to load multiple programs into the same physical address space but became less useful as we moved to virtual address spaces
Shared libraries and PIC
- With shared libraries we have the need to use PIC again since we don’t want shared libraries to be overlapping and we do not know which virtual address that shared library will run at.
- Before PIC we’re using load time relocation for shared libraries which made the dynamic linker do a lot of relocation during runtime.
- PIC eliminates all absolute addressing
- Replaces w either relative addressing or a small jump table
- Things can be forked and the same shared library will run at different address, depending on the decisions made by the dynamic linker.
PIE (Position Independent Executables)
- Before the early 2000s, PIC was limited to shared libaries.
- Then PIE came to solve some security concerns (allowing ASLR).
- PIE executables are sort of a hack using a shared object with an entry-point which do be using relative addressing instead of absolute addressing.
- Interesting thing is PIE can be done in both the code generation phase(
-fPIE
) and in the linking phase(-pie
). - Other than security usage, PIE can also have other uses like making binaries more appropriate for MMU-less systems.
- The presence of the
PHDR
andINTERP
program headers(readelf -l
) indicates that the executable is PIE, this is probably whatfile
uses. - Additionally,
Scrt1.o
is used in place ofcrt1.o
when generating PIEs but a shared library is normally not. But there is nothing to prevent a shared library to be linked withScrt1.o
as well
ASLR
- Applications run in a randomized address space.
- Out of many things, having ASLR makes it hard for the attacker to guess the address space of a program.
- Eg. NX bit cannot prevent return to libc attacks, but ASLR can make it very difficult to perform.
- When running
gdb
, ASLR is usually disabled. So if you want to rungdb
with ASLR, you might want to use the processpid
λ sudo sysctl -a | grep 'randomize_va_space'
146:kernel.randomize_va_space = 2
λ ldd a.out
linux-vdso.so.1 (0x00007ffc37974000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fe87a9df000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fe87abe1000)
λ ldd a.out
linux-vdso.so.1 (0x00007ffe6e1f9000) # notice the address change.
libc.so.6 => /usr/lib/libc.so.6 (0x00007f1e91303000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f1e91505000)
PIC vs PIE
- The only real difference between PIE and PIC is that you are allowed to interpose symbols in PIC, but not in PIE.
- They are pretty much equivalent except some optimizations that are sort of negligible, but the produced code has certain places that it can be used as mentioned in this table
Library Intent Table
ELF Type | Intent | Note |
---|---|---|
Not ELF | Static Library (.a ) | Static libraries are just archives(.a ) which can contain any or mix of PIE/PIC/non-PIC relocatable object files, but mixing is usually not the best thing to do. |
ET_REL | Relocatable Object Files (.o ) | These are generated based on code generation options available. -fPIC , -fPIE , -fno-pie |
ET_DYN | Shared Library (.so ) | Code used for shared libraries must be generated using -fPIC , if the shared library is supposed to link against another static library the object files in the static library should be compiled using -fPIC aswell. |
ET_DYN | Static PIE Exec | May only be created from PIC/PIE objects (.o ) and PIC/PIE static libraries (.a ), this was not possible until the -static-pie flag was added. But this is something one would not use often. ldd shows statically linked but the ELF type is ET_DYN ! weird. |
ET_EXEC | Static non-PIE Exec | This is the traditional way how static libraries are used as they already have the symbols resolved. The objects here can be non-PIC/PIC/PIE. One simply needs to use the -static flag for this. |
ET_DYN | Dynamic PIE Exec | This is the most common thing one would encounter. May only be created from PIC/PIE objects (.o) and PIC/PIE static libraries (.a) It uses -pie which is added by default in GCC these days. |
ET_EXEC | Dynamic non-PIE Exec | One needs to use -no-pie flag, the object files can be PIC/non-PIC/PIE. ldd will show the dynamically linked libraries. |
So now what?
- Code compiled with
-fPIC
can be used in anything, from shared libraries to executables (PIE or not) - non-PIE executables can be created from any objects or static libraries.
- If both static and shared libraries are found, the linker gives preference to linking with the shared library unless the
-static
option is used. - When creating PIE executables its better to use
-fPIE
for code generation for some small optimizations over-fPIC
. - When creating non-PIE executables using PIC/PIE object file is possible but makes less sense and adds some bloat.
- Mixing of non-PIC and PIC objects is for sure a very weird usecase
- To enable ASLR for executables, they extend PIC to and called it PIE.
- When compile w/o PIE (
--no-pie
linker option) you get an ELF object of typeET_EXEC
- When compile w PIE (
-pie
, default nowadays) you get the executable with typeET_DYN
, which is the same type as of shared objects. - It appears that the main effect of
ET_EXEC
vsET_DYN
in the Linux kernel / dynamic loader is to inform if the executable can be placed in random memory locations or not with ASLR. - Honestly, I don’t even know.
Linux Processes
PLT (Procedure Linkage Table)
- Used for procedures
- Used to call external procedures/functions whose address isn’t known in the time of linking, and is left to be resolved by the dynamic linker.
- PLT and
PIC/PIE
are not really related. PIE
executables can useGOT
lookup if it wants to- Using a PLT tends to make the code slightly more efficient though.
- The dynamic linker mangles w
GOT
/PLT
to correctly point the shared library symbols for the running program. - Usually placed in
.plt
section in a read-onlysegment
GOT (Global Offset Table)
- Used for global and static variables
- Modern operating systems has 2 GOT for each process.
- One is named
.got
and the other.got.plt
(writable segment).
-
Compilation
- There is one GOT per compilation unit or object module
- It is located at a fixed offset from the code (although this offset is not known until the library is linked).
- For every reference to a global variable from PIC
- Compiler will generate a load from the GOT to get the address of the variable
- Followed by a second load to get the actual value of the variable.
-
Linking
- When a linker links modules to create a shared library, it merges the GOTs and sets the final offsets in code. Just like the linker merges the
.text
sections into one. - Static Linker will add stuff in
.dynsym
which the dynamic linker will use to initialize the GOT at runtime. - Unlike PLT, the dynamic linker always fully initializes the GOT when the program starts, it’s only lazy loading for PLT these days.
- When a linker links modules to create a shared library, it merges the GOTs and sets the final offsets in code. Just like the linker merges the
PLT vs GOT
- Used for procedures vs Used for variables
- PLT and GOT
- Confusion about PIC and virtual address space
- What does @plt mean
- glibc at runtime
Misc
Making small executables
- The symbol table(
.symtab
) can be stripped for executables or shared objects because it is neither used by the dynamic linker(ld.so
) not by the os loader. - It’s only used by the static linker/link-editor(
ld
). - Relocation info(
.rel_text
,.rel_data
) already stripped by the linker after relocating. - One could also use “-n” (also known as “-nmagic”) linker option. This basically tells
ld
to not worry about aligning program sections on page boundaries. - Further to strip, one could also use
sstrip
.
How memory is laid out
- The starting point address is part of a set of conventions which describe how memory is laid out.
- The linker, when it produces an executable binary, must know these conventions as it dictates how the executable is formed.
- This memory layout conventions can be changed if needed.
- On Linux, the default address for
x86_64
binaries is0x400000
(4MiB), and so the.text
ends up not far from there.
# x86_64 chooses 0x400000
# 32bit chooses 0x08048000
$ cat /usr/lib/ldscripts/elf_x86_64.x|rg 400000
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
$ ld -verbose | rg -i text-segment
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
- Why exactly 0x400000 has more to it
vdso(vDSO)
- A way to export kernel space routines to userspace.
- The main reason is to reduce the syscalls overhead.
- Typically when a system call happens it requires some expensive operations like switching mode from user to kernel, copying data from userspace to kernelspace etc.
- To reduce these sorts of overhead vDSO is used, just by reading that vdso memory space result could be extracted i.e it’s possible to
gettimeofday()
without doing a real system call! linux-vdso.so.1
is a virtual library that is automatically mapped in the address space of a process by the kernel, it does not have a filename but has and address when we doldd
.
λ ldd /bin/ls
linux-vdso.so.1 (0x00007ffc40f03000) # no filename!
libcap.so.2 => /usr/lib/libcap.so.2 (0x00007fcf9616a000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fcf95fa4000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fcf961cf000)
Different Libraries
Links
- Linking
- ELF: dynamic struggles
- Tools for examining different phases of compiling and running a C program
- Hosted vs Freestanding Environments
- 20 Part Seies on Linkers and Loaders
- Linkers and loaders book.
- About ELF — PIE, PIC and else
- X86 psABI
- Building And Using Static And Shared “C” Libraries
- various mechanisms to protect against buffer overflow exploits.
- Traditional Unix Toolchain
- https://begriffs.com/posts/2021-07-04-shared-libraries.html?hn=2
- https://seenaburns.com/building-c-programs/
Tools
nm
, size
, file
, execstack
, readelf
, ldd
, hexdump
, objdump