The dissection of a simple "hello world" ELF binary.
The representation of executables, shared libraries and relocatable object code is standardized by a variety of file formats which provides encapsulation of assembly instructions and data. Two such formats are the Portable Executable (PE) file format and the Executable and Linkable Format (ELF), which are used by Windows and Linux respectively. Both of these formats partition executable code and data into sections and assign appropriate access permissions to each section, as summarised by table 1. In general, no single section has both write and execute permissions as this could compromise the security of the system.
|Section name | Usage description | Access permissions| |-------------|-----------------------|-------------------| |
.text| Assembly instructions |
.rodata| Read-only data |
.data| Data |
.bss| Uninitialized data |
Table 1: A summary of the most commonly used sections in ELF files. The
.textsection contains executable code while the
.bsssections contains data in various forms.
To gain a better understanding of the anatomy of executables the remainder of this section describes the structure of ELF files and presents the dissection of a simple "hello world" ELF executable, largely inspired by Eric Youngdale's article on The ELF Object File Format by Dissection. Although the ELF and PE file formats differ with regards to specific details, the general principles are applicable to both formats.
In general, ELF files consist of a file header, zero or more program headers, zero or more section headers and data referred to by the program or section headers, as depicted in figure 1.
Figure 1: The basic structure of an ELF file.
All ELF files starts with the four byte identifier
'F'which marks the beginning of the ELF file header. The ELF file header contains general information about a binary, such as its object file type (executable, relocatable or shared object), its assembly architecture (x86-64, ARM, …), the virtual address of its entry point which indicates the starting point of program execution, and the file offsets to the program and section headers.
Each program and section header describes a continuous segment or section of memory respectively. In general, segments are used by the linker to load executables into memory with correct access permissions, while sections are used by the compiler to categorize data and instructions. Therefore, the program headers are optional for relocatable and shared objects, while the section headers are optional for executables.
Figure 2: The entire contents of a simple "hello world" ELF executable with colour-coded file offsets, sections, segments and program headers. Each file offset is 8 bytes in width and coloured using a darker shade of its corresponding segment, section or program header.
To further investigate the structure of ELF files a simple 64-bit "hello world" executable has been dissected and its content colour-coded. Each file offset of the executable consists of 8 bytes and is denoted in figure 2 with a darker shade of the colour used by its corresponding target segment, section or program header. Starting at the middle of the ELF file header, at offset
0x20, is the file offset (red) to the program table (bright red). The program table contains five program headers which specify the size and file offsets of two sections and three segments, namely the
.interp(gray) and the
.dynamic(purple) sections, and a read-only (blue), a read-write (green) and a read-execute (yellow) segment.
Several sections are contained within the three segments. The read-only segment contains the following sections:
.interp: the interpreter, i.e. the linker
.dynamic: array of dynamic entities
.dynstr: dynamic string table
.dynsym: dynamic symbol table
.rela.plt: relocation entities of the PLT
.rodata: read-only data section
The read-write segment contains the following section:
.got.plt: Global Offset Table (GOT) of the PLT (henceforth referred to as the GOT as this executable only contains one such table)
And the read-execute segment contains the following sections:
.plt: Procedure Linkage Table (PLT)
.text: executable code section
Seven of the nine sections contained within the executable are directly related to dynamic linking. The
.interpsection specifies the linker (in this case "/lib/ld64.so.1") and the
.dynamicsection an array of dynamic entities containing offsets and virtual addresses to relevant dynamic linking information. In this case the dynamic array specifies that "libc.so.6" is a required library, and contains the virtual addresses to the
.got.pltsections. As noted, even a simple "hello world" executable requires a large number of sections related to dynamic linking. Further analysis will reveal their relation to each other and describe their usage.
The dynamic string table contains the names of libraries (e.g. "libc.so.6") and identifiers (e.g. "printf") which are required for dynamic linking. Other sections refer to these strings using offsets into
.dynstr. The dynamic symbol table declares an array of dynamic symbol entities, each specifying the name (e.g. offset to "printf" in
.dynstr) and binding information (local or global) of a dynamic symbol. Both the
.rela.pltsections refers to these dynamic symbols using array indicies. The
.rela.pltsection specifies the relocation entities of the PLT; more specifically this section informs the linker of the virtual address to the
.exitentities in the GOT.
To reflect on how dynamic linking is accomplished on a Linux system lets review the assembly instructions of the executable
.pltsections as outlined by figure 3 and 4 respectively.
text: .start: mov rdi, rodata.hello call plt.printf mov rdi, 0 call plt.exit
Figure 3: The assembly instructions of the
plt: .resolve: push [got_plt.link_map] jmp [got_plt.dl_runtime_resolve] .printf: jmp [got_plt.printf] .resolve_printf: push dynsym.printf_idx jmp .resolve .exit: jmp [got_plt.exit] .resolve_exit: push dynsym.exit_idx jmp .resolve
Figure 4: The assembly instructions of the
As visualized in figure 3 the first call instruction of the
.textsection targets the
.printflabel of the
.pltsection instead of the actual address of the printf function in the libc library. The Procedure Linkage Table (PLT) provides a level of indirection between call instructions and actual function (procedure) addresses, and contains one entity per external function as outlined in figure 4. The
.printfentity of the PLT contains a jump instruction which targets the address stored in the
.printfentity of the GOT. Initially this address points to the next instruction, i.e. the instruction denoted by the
.resolve_printflabel in the PLT. On the first invokation of printf the linker replaces this address with the actual address of the printf function in the libc library, and any subsequent invokation of printf will target the resolved function address directly.
This method of external function resolution is called lazy dynamic linking as it postpones the work and only resolves a function once it is actually invoked at runtime. The lazy approach to dynamic linking may improve performance by limiting the number of symbols that require resolution. At the same time the eager approach may benefit latency sensitive applications which cannot afford the cost of dynamic linking at runtime.
A closer look at the instructions denoted by the
.resolve_printflabel in figure 4 reveals how the linker knows which function to resolve. Essentially the dlruntimeresolve function is invoked with two arguments, namely the dynamic symbol index of the printf function and a pointer to a linked list of nodes, each refering to the
.dynamicsection of a shared object. Upon termination the linked list of our "hello world" process contains a total of four nodes, one for the executable itself and three for its dynamically loaded libraries, namely linux-vdso.so.1, libc.so.6 and ld64.so.1.
To summarise, the execution of a dynamically linked executable can roughly be described as follows. Upon execution the kernel parses the program headers of the ELF file, maps each segment to one or more pages in memory with appropriate access permissions, and transfers the control of execution to the linker ("/lib/ld64.so.1") which was loaded in a similar fashion. The linker is responsible for initiating the addresses of the dlruntimeresolve function and the aforementioned linked list, both of which are stored in the GOT of the executable. After this setup is complete the linker transfers control to the entry point of the executable, as specified by the ELF file header (in this case the
.startlabel of the
.textsection). At this point the assembly instructions of the application are executed until termination and external functions are lazily resolved at runtime by the linker through invokations to the dlruntimeresolve function.
The source code and any original content of this repository is hereby released into the public domain.
The original version of
elf_structure.pngis licensed CC-BY.