ELF Story Part5: ELF Segments

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.

Leave a Reply

Your email address will not be published. Required fields are marked *