1- Introduction
In the previous sections, I wrote about ELF sections and provided detailed descriptions of them. I discussed the ELF header and the section headers, all of which play a crucial role during the linking and compiling processes to create the ELF file. We now understand how the ELF file is generated and how the linker and compiler work together to assemble ELF files.
In this part, I want to explain how the ELF file is loaded into memory at runtime. To do this, we need to understand ELF segments, as the dynamic linker or loader uses these segments to map the ELF file into memory. Let’s take a closer look at ELF segments in ELF files.
2- ELF Segments
The section view of an ELF binary is meant for static linking purposes only.
Sections and Segments both operate on the same data, but they index the data of the ELF file differently.
On the other hand, the segment view, which I’ll discuss next, is used by the operating system and dynamic linker when loading an ELF into a process for execution to locate the relevant code and data and decide what to load into virtual memory.
The part of an ELF file that manages the segments is called the “Program Header Table.” This table informs the system how to construct a process image.
Files intended to execute a program must include a program header table, while relocatable (object) files do not require one.
To summarize, an ELF (Executable and Linkable Format) file contains both data and machine code for a program, which is divided into multiple parts.
The ELF format offers two distinct views of this content: segments and sections.
It’s important to note that these are simply perspectives on the data within the file. They do not define the content itself but serve as an index to it.
To map the content of an executable file into a process’s memory, we utilize a table within the ELF (Executable and Linkable Format) structure known as the Program Header Table or Segment Header Table.
This table serves as a guide for the dynamic loader, which is responsible for loading the executable into memory. Let’s look at a sample. Below, you see the output of the readelf tool. The list of segments of an executable file is printed.
alee@debian:~$ readelf --segments -W ./a.out
Elf file type is DYN (Shared object file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x000000040 0x00000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x000000318 0x00000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x000000000 0x00000000 0x000658 0x000658 R 0x1000
LOAD 0x001000 0x000001000 0x00001000 0x000245 0x000245 R E 0x1000
LOAD 0x002000 0x000002000 0x00002000 0x000238 0x000238 R 0x1000
LOAD 0x002d98 0x000003d98 0x00003d98 0x000278 0x000280 RW 0x1000
DYNAMIC 0x002dc8 0x000003dc8 0x00003dc8 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000338 0x000000338 0x00000338 0x000020 0x000020 R 0x8
NOTE 0x000358 0x000000358 0x00000358 0x000044 0x000044 R 0x4
GNU_PROPERTY 0x000338 0x000000338 0x00000338 0x000020 0x000020 R 0x8
GNU_EH_FRAME 0x002048 0x000002048 0x00002048 0x000064 0x000064 R 0x4
GNU_STACK 0x000000 0x000000000 0x00000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002d98 0x000003d98 0x00003d98 0x000268 0x000268 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag
.gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
.rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
Segments provide an execution view and are necessary only for executable ELF
files, not for non-executable files like relocatable objects.
The program header table encodes the segment view using the program headers of
type struct Elf64_Phdr.
typedef struct {
uint32_t p_type; /* Segment type */
uint32_t p_flags; /* Segment flags */
uint64_t p_offset; /* Segment file offset */
uint64_t p_vaddr; /* Segment virtual address */
uint64_t p_paddr; /* Segment physical address */
uint64_t p_filesz; /* Segment size in file */
uint64_t p_memsz; /* Segment size in memory */
uint64_t p_align; /* Segment alignment */
} Elf64_Phdr;
1-1- p_type
This member tells what kind of segment this array element describes
or how to interpret the array element’s information.
Name Value
PT_NULL 0
PT_LOAD 1
PT_DYNAMIC 2
PT_INTERP 3
PT_NOTE 4
PT_SHLIB 5
PT_PHDR 6
PT_TLS 7
PT_LOOS 0x60000000
PT_HIOS 0x6fffffff
PT_LOPROC 0x70000000
PT_HIPROC 0x7fffffff
PT_NULL: The array element is unused; other members’ values are undefined.
This type lets the program header table have ignored entries.
PT_LOAD: As the name implies, these are intended to be loaded into memory when setting up the process.
The size of the loadable chunk and the address to load it are described in the rest of the program header.
PT_DYNAMIC: The array element specifies dynamic linking information.
The PT_DYNAMIC segment contains the .dynamic section, which instructs the
interpreter on how to parse and prepare the binary for execution.
PT_INTERP: The PT_INTERP segment includes the .interp section, which
specifies the interpreter to be used for loading the binary.
As you see in the readelf output, this entry contains the path of the
dynamic loader, which is in the .interp section.
PT_NOTE: The array element specifies the location and size of the auxiliary
information.
PT_SHLIB: This segment type is reserved but has unspecified semantics.
PT_PHDR: The array element, if present, specifies the location and size of
the “program header table” itself, both in the file and in the memory image
of the program. This segment type may not occur more than once in a file.
PT_TLS: The array element specifies the Thread-Local Storage template.
PT_LOOS-PT_HIOS: Values in this inclusive range are reserved for operating
system-specific semantics.
PT_LOPROC-PT_HIPROC: Values in this inclusive range are reserved for
processor-specific semantics. If meanings are specified, the processor
supplement explains them.
1-2- p_flags:
The flags indicate the runtime access permissions for the segment. There are three important types of flags: PF_X, PF_W, and PF_R.
The PF_X flag signifies that the segment is executable.
It is typically set for code segments and in the output of the readelf
command, it is displayed as an “E” rather than an “X” in the “Flg” column.
The PF_W flag indicates that the segment is writable.
It is usually applied only to writable data segments and is never used for
code segments.
The PF_R flag denotes that the segment is readable, which is generally the
case for both code and data segments.
1-3- The p_offset, p_vaddr, p_paddr, p_filesz, and p_memsz Fields:
The fields p_offset, p_vaddr, and p_filesz correspond to the sh_offset,
sh_addr, and sh_size fields found in a section header. Specifically, these
fields define the following:
p_offset indicates the file offset where the segment begins,
p_vaddr denotes the virtual address at which the segment should be loaded,
p_filesz specifies the file size of the segment.
For loadable segments, p_vaddr must be equal to p_offset when calculated
modulo the page size, which is typically 4,096 bytes.
In certain systems, the p_paddr field can be used to indicate the specific
address in physical memory where a segment should be loaded. However, in
modern operating systems like Linux, this field is not used and is set to
zero, as all binaries are executed in virtual memory.
At first glance, it may not be immediately clear why there are separate
fields for the file size of a segment (p_filesz) and its size in memory
(p_memsz).
To understand this distinction, consider that some sections indicate the
need to allocate certain bytes in memory, but do not include these
bytes in the binary file.
A common example is the .bss section, which contains zero-initialized data.
Since all the data in this section is already known to be zero, there is no
need to physically include all these zeros in the binary file.
However, when the segment containing .bss is loaded into virtual memory, all
The bytes in .bss must be allocated.
Consequently, p_memsz can be larger than p_filesz.
In such cases, the loader adds the extra bytes at the end of the segment
During the binary loading process, and initializes them to zero.
1-4- The p_align
The “p_align” field is similar to the “sh_addralign” field found in a section header.
It specifies the required memory alignment, measured in bytes, for the
segment. As with “sh_addralign”, an alignment value of 0 or 1 indicates that
no specific alignment is necessary.
If “p_align” is set to a value other than 0 or 1, it must be a power of 2.
Additionally, the value of “p_vaddr” must be equal to “p_offset” when taken
modulo “p_align”.
Typically, the readelf output displays at least two PT_LOAD segments, one
for non-writable sections and another for writable data sections.
On Disk In Memory
+------------------+ +-----------------+
|ELF Header | | PT_LOAD |
| |------->| R_E |
|------------------| |-----------------|
|Program Hdr Table | | PHDR |
| |------->| R_E |
|------------------| |-----------------|
| .interp | | INTERP |
| |------->| R__ |
|------------------|----- |-----------------|
| .plt | | | |
| | | | |
-------------------| | | |
| .rodata | | | PT_LOAD |
| | |-->| R_E |
|------------------| | | |
| | | | |
| .text | | | |
| | | | |
| | | | |
|------------------|----- |-----------------|
| .dynamic | | DYNAMIC |
| |------->| RW_ |
|------------------|----- |-----------------|
| .got | | | |
| | | | |
-------------------| | | |
| .data | |-->| |
| | | | PT_LOAD |
|------------------| | | RW |
| .bss | | | |
|------------------|---- | |
| .symtab | | |
|------------------| |-----------------|
| .strtab | | Alignment |
| | | |
+------------------+ +-----------------+
The final executable file, a.out, is illustrated as follows. The linker view
on the left displays how each section is stored as a file on the disk, while
the loader view on the right shows how each segment is loaded as a process
during runtime. For example, the first section, labeled [01], has a size of
0x1c and is allocatable (A) by the loader. The designations R, X, and W
signify readable, executable, and writable, respectively.
In the loader view, there are four main chunks of memory:
0x400000-0x401000 (RX)
0x401000-0x402000 (RW)
Regions for shared objects
Areas allocated for the stack and heap.
Linker View Loader View
FileOffset VirtualAddr LOAD (SZ=0x1000)
.-----------------------------. ------------> .------------------.
0x0000| ELF Header | 0x400000 | ELF_HDR (R) |
.-----------------------------. |------------------|
0x0040| Program Header Table | 0x400040 | PHDR (R) |
| | | |
| | | |
| | | |
| | | |
.-----------------------------. |------------------|
0x0200| [01] .interp (0x1c) A | 0x400200 | INTERP (R) |
.-----------------------------. ------------> |------------------|
0x021c| [02] .note.ABI-tag (0x20) A | | LOAD (R) |
.-----------------------------. | |
0x0240| [03] .dynsym (0xa8) A | | |
.-----------------------------. | |
0x02e8| [04] .dynstr (0x89) A | | |
.-----------------------------. | |
0x0378| [05] .hash (0x30) A | | |
.-----------------------------. | |
0x03a8| [06] .gnu.version (0xe) A | | |
.-----------------------------. | |
0x03b8| [07] .gnu.version_r (0x20) A| | |
.-----------------------------. | |
0x03d8| [08] .rela.dyn (0x18) A | | |
.-----------------------------. | |
0x03f0| [09] .rela.plt (0x48) AI | | |
.-----------------------------. ------------> |------------------|
0x0438| [10] .init (0x1a) AX | | LOAD(RX) |
.-----------------------------. | |
0x0460| [11] .plt (0x40) AX | | |
.-----------------------------. | |
0x04a0| [12] .text (0xa8) AX | | |
| | | |
.-----------------------------. | |
0x06f4| [13] .fini (0x9) AX | | |
.-----------------------------. ------------> |------------------|
0x0700| [14] .rodata (0x1c) AMS | | |
.-----------------------------. | |
0x0720| [15] .eh_frame (0xd4) A | | |
.-----------------------------. | |
0x07f4| [16] .eh_frame_hdr (0x2c) A | | |
.-----------------------------. ------------> .------------------.
0x0820| [17] .dynamic (0x1d) WA | |
| | | LOAD (SZ=0x1000)
.-----------------------------. | .------------------.
0x09f0| [18] .got (0x8) WA | | 0x401000 | |
.-----------------------------. \--------> .------------------.
0x09f8| [19] .got.plt (0x30) WA | 0x401820 | (RW) |
.-----------------------------. | |
0x0a28| [20] .data (0x10) WA | | |
.-----------------------------. | |
0x0a38| [21] .jcr (0x8) WA | | |
.-----------------------------. | |
0x0a40| [22] .tm_clone_tbl (0x0)WA | | |
.-----------------------------. | |
0x0a40| [23] .fini_array (0x8) WA | | |
.-----------------------------. | |
0x0a48| [24] .init_array (0x8) WA | | |
.-----------------------------. | |
0x0a50| [25] .bss (0x4) WA | 0x401a50 | |
.-----------------------------. ---> 0x402000 .------------------.
0x0a50| [26] .comment (0x62) MS | \
.-----------------------------. | .------------------.
0x0ab4| [27] .note.gnu.gold (0x1c) | | 7ffff7a0e000| Shared Objects |
.-----------------------------. | | libc, ld, ... |
0x0ad0| [28] .symtab (0x408) | | | |
.-----------------------------. | .------------------.
0x0ed8| [29] .strtab (0x23f) | |
.-----------------------------. | --> No mapping in a virtual address
0x1117| [30] .shstrtab (0x118) | |
.-----------------------------. | .------------------.
0x1230| Section Header Table | | | Heap |
| | | | |
| | | | |
| | | 7ffffffde000| Stack |
| | | .------------------.
0x19f0 .-----------------------------./
As illustrated in the figure above, we have a file containing some data. This data is organized in two different formats: one for static linking and another for dynamic loading.
For the static linker, the organization is managed by Section Header Tables, which categorize binary data into specific tables. In contrast, the dynamic loader relies on Program Header Tables for its organization. We can visualize an ELF (Executable and Linkable Format) file as having two distinct aspects: one for storage on the disk and the other for execution in memory. The disk aspect is represented by Section Headers, while the memory aspect is represented by Program Headers.
Now let’s have an example for the segmentation view in the ELF files.
We have compiled the ELF file main.out. We can see the contents of the segments of the file with the readelf command.
alee@laptop-debian:~/$ readelf --program-headers main.out -W
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 11 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x00000040 0x00000040 0x000268 0x000268 R 0x8
INTERP 0x0002a8 0x000002a8 0x000002a8 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x00000000 0x00000000 0x0005c0 0x0005c0 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x00027d 0x00027d R E 0x1000
LOAD 0x002000 0x00002000 0x00002000 0x0001a8 0x0001a8 R 0x1000
LOAD 0x002de8 0x00003de8 0x00003de8 0x000250 0x000258 RW 0x1000
DYNAMIC 0x002df8 0x00003df8 0x00003df8 0x0001e0 0x0001e0 RW 0x8
NOTE 0x0002c4 0x000002c4 0x000002c4 0x000044 0x000044 R 0x4
GNU_EH_FRAME0x002038 0x00002038 0x00002038 0x000044 0x000044 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002de8 0x00003de8 0x00003de8 0x000218 0x000218 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.build-id .note.ABI-tag
08 .eh_frame_hdr
09
10 .init_array .fini_array .dynamic .got
Now I execute the main.out file and get its process ID to find its memory mapping.
alee@laptop-debian:~/$ ps -eaf|grep main.out|grep -v "grep"
alee 2697 2685 0 19:32 pts/4 00:00:00 ./main.out
Now we can find memory mapping in the /proc virtual directory
alee@laptop-debian:~$ cat /proc/2697/maps
55bfccb58000-55bfccb59000 r--p 00000000 103:0b 7223095 /home/alee/elfs_story/codes/main.out
55bfccb59000-55bfccb5a000 r-xp 00001000 103:0b 7223095 /home/alee/elfs_story/codes/main.out
55bfccb5a000-55bfccb5b000 r--p 00002000 103:0b 7223095 /home/alee/elfs_story/codes/main.out
55bfccb5b000-55bfccb5c000 r--p 00002000 103:0b 7223095 /home/alee/elfs_story/codes/main.out
55bfccb5c000-55bfccb5d000 rw-p 00003000 103:0b 7223095 /home/alee/elfs_story/codes/main.out
55bfce566000-55bfce587000 rw-p 00000000 00:00 0 [heap]
7fdf299ce000-7fdf299d1000 rw-p 00000000 00:00 0
7fdf299d1000-7fdf299f7000 r--p 00000000 103:0a 2623650 /usr/lib/x86_64-linux-gnu/libc.so.6
7fdf299f7000-7fdf29b4c000 r-xp 00026000 103:0a 2623650 /usr/lib/x86_64-linux-gnu/libc.so.6
7fdf29b4c000-7fdf29b9f000 r--p 0017b000 103:0a 2623650 /usr/lib/x86_64-linux-gnu/libc.so.6
7fdf29b9f000-7fdf29ba3000 r--p 001ce000 103:0a 2623650 /usr/lib/x86_64-linux-gnu/libc.so.6
7fdf29ba3000-7fdf29ba5000 rw-p 001d2000 103:0a 2623650 /usr/lib/x86_64-linux-gnu/libc.so.6
7fdf29ba5000-7fdf29bb2000 rw-p 00000000 00:00 0
7fdf29bc4000-7fdf29bc6000 rw-p 00000000 00:00 0
7fdf29bc6000-7fdf29bc7000 r--p 00000000 103:0a 2623646 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fdf29bc7000-7fdf29bec000 r-xp 00001000 103:0a 2623646 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fdf29bec000-7fdf29bf6000 r--p 00026000 103:0a 2623646 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fdf29bf6000-7fdf29bf8000 r--p 00030000 103:0a 2623646 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fdf29bf8000-7fdf29bfa000 rw-p 00032000 103:0a 2623646 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe96600000-7ffe96621000 rw-p 00000000 00:00 0 [stack]
7ffe9665b000-7ffe9665f000 r--p 00000000 00:00 0 [vvar]
7ffe9665f000-7ffe96661000 r-xp 00000000 00:00 0 [vdso]
The /proc/maps file of a process provides a detailed overview of the memory regions allocated to that process, organized into six distinct columns.
1. Virtual Address Range: The first column displays the start and end addresses of each memory segment, indicating the range of virtual memory allocated.
2. Permissions: The second column outlines the access permissions associated with each memory region, such as read, write, and execute permissions, which govern how the process can interact with that memory.
3. Offset: The third column indicates the offset within the file that the memory region maps to, allowing for a better understanding of how the memory is related to the program’s binary.
4. Device: The fourth column describes the device associated with the memory mapping, providing insight into whether the memory is mapped to a file or is anonymous.
5. Inode: The fifth column states the inode number of the file being mapped, which is useful for identifying the file within the filesystem.
6. Path: Finally, the sixth column provides the pathname of the file corresponding to the memory mapping, if applicable, or it indicates that the region is anonymous. This structured information is invaluable for developers and system administrators to monitor memory usage and diagnose potential issues within a running process.
The program header plays a crucial role in aiding the dynamic loader to map the contents of an ELF (Executable and Linkable Format) file into virtual memory.
For instance, if you examine the second row of the /proc/maps file, you will note that the memory region starts at the offset of 0x1000, which is allocated with Read and Execute permissions.
The code located at the specified address can be read and executed by the processor.
If you analyze the output of the `readelf` command, particularly the segments view, you’ll find a LOAD entry corresponding to the offset of 0x1000.
This LOAD entry is the third entry(counting from zero).
In the “Section to Segment mapping” section of the `readelf` output, the third row indicates the sections (.init, .plt, .plt.got, .text, .fini) that should be loaded with that LOAD entry, which has Read and Execute permissions.
This LOAD segment specifies Read and Execute permissions, confirming that the code can be safely read from memory and executed. The alignment between the information in `/proc/maps` and the `readelf` output illustrates how the dynamic loader uses the program header to ensure the proper mapping and execution of the ELF file in the system’s virtual memory.
Concolusion
In summary, understanding ELF files—particularly their sections, headers, and segments—is crucial for grasping how programs are compiled, linked, and executed.
The ELF header and section headers organize the file, while the segments determine how code and data are loaded into memory.
The relationship between outputs from tools like `readelf` and `/proc/maps` illustrates the dynamic loader’s key role in ensuring everything is correctly mapped for execution. Mastering the ELF format enhances programming skills and provides insight into system operations, making it essential for developers and systems engineers focused on building efficient applications.