tags : Systems, elf

C compilation process

credits

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. But ELF 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 that gcc runs. (Note: collect2 is alias for ld)
$ 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 general gcc 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.
  • 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 the ELF segments etc.
    • Once the segments are loaded, the crt0 (.s / .o) can do its initialization stuff (populate argv etc.)
      • crt0 setup the C runtime environment and then call main (libc.so.6:__libc_start_main then calls out main).
      • 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 the crt0 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 same segment 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

Static Linker

  • Scripts (LD)
  • You can provide a custom script
  • Link editor may provide a script for static linking

Dynamic Linker

  • 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

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 the obj file.
    • It creates the symbol table in the .symtab section of the ELF object.
    • In ELF64, .symtab contains an array entries of struct Elf64_Sym.
  • ld (linker) will look into .symtab, .rel.text, .rel.data to perform symbol resolution and relocation
    • Linker puts sections into segments
    • For dynamic executables, it’ll leave some of the linking to the loader/dynamic linker. (Partial Linking)
  • 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 using GOT and PLT
    • We could also use -fno-plt to resolve all external symbols at load time.

Symbol Table

  • The assembler creates the symbol table in the .symtab section of the ELF object. .o file will contain the sections
  • .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 struct
    typedef 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 as UNDEF, COMMON, ABS etc.)

More on st-info

  • Bindings

    Also see Language 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 by m, but by some other module. STB_GLOBAL
      • Local Symbols : Defined and referenced exclusively by m, STB_LOCAL
      staticnon-static
      Global VariableLocal SymbolGlobal Symbol
      Local VariableLocal Symbol w unique name(stored in .data / .bss)Maintained at runtime on the stack
      Global FunctionLocal SymbolGlobal Symbol
      Local FunctionNo such thingNo such thing

Dynamic Symbol Table

Where?

  • .dynsym is found in shared objects and dynamic 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.
  • The assembler creates the symbol table in the .symtab section of the ELF object. .o file will contain the sections. .symtab entries are also referred to as linker symbols
  • Static Linker uses the st-info and reloc. 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 how ld 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 in readelf -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

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 in gcc’s man page as such though. One could use md5sum / 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 Makefiles
  • LD_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 users
  • LD_PRELOAD: It lists shared libraries with functions that override the standard set, just like /etc/ld.so.preload.
  • LD_DEBUG : Triggers the dl* 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 update ld.so.conf
  • /etc/ld.so.preload : Serves the same purpose as setting the LD_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 update ld.so.conf and run ldconfig 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 and INTERP program headers(readelf -l) indicates that the executable is PIE, this is probably what file uses.
  • Additionally, Scrt1.o is used in place of crt1.o when generating PIEs but a shared library is normally not. But there is nothing to prevent a shared library to be linked with Scrt1.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 run gdb with ASLR, you might want to use the process pid
λ 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 TypeIntentNote
Not ELFStatic 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_RELRelocatable Object Files (.o)These are generated based on code generation options available. -fPIC, -fPIE, -fno-pie
ET_DYNShared 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_DYNStatic PIE ExecMay 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_EXECStatic non-PIE ExecThis 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_DYNDynamic PIE ExecThis 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_EXECDynamic non-PIE ExecOne 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 type ET_EXEC
  • When compile w PIE (-pie, default nowadays) you get the executable with type ET_DYN, which is the same type as of shared objects.
  • It appears that the main effect of ET_EXEC vs ET_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 use GOT 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-only segment

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.

PLT vs GOT

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 is 0x400000 (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;

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 do ldd.
λ 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

Tools

nm, size, file, execstack, readelf, ldd, hexdump, objdump