ELF sections, segments, and their mapping in memory
This article’s aim is to generally explain what sections and segments are in an ELF file on x86-64 GNU/Linux and how they are mapped to memory. It’s a good practice to understand ELF file structure and virtual memory mapping; this article also serves as my personal note.
Program headers and PT_LOAD segments
Program headers, preceded by ELF header, are essentially an array of structures that can be parsed to show the mapping between sections (within ELF file) and segments (loaded into memory).
The diagram above shows a panoramic view of an ELF file structure. Now, I want to know exactly where program headers reside and what each header contains1. It’s always a good way to start by compiling a simple program and examining it with tools like readelf
, gdb
, IDA
, etc., to understand the file structure.
1
2
3
4
5
6
7
// test.c
#include <stdio.h>
int main(){
printf("hello world!, this is function: %s\n", __FUNCTION__);
return 0;
}
// Usage: gcc test.c -o test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; IDA View-A
[ ... ] skipping ELF header
LOAD:0000000000000040 ; ELF64 Program Header
[ ... ] skipping PHT entry 0 - 2
LOAD:00000000000000E8 ; PHT Entry 3
LOAD:00000000000000E8 dd 1 ; Type: LOAD
LOAD:00000000000000EC dd 5 ; Flags
LOAD:00000000000000F0 dq 1000h ; File offset
LOAD:00000000000000F8 dq offset _init_proc ; Virtual address
LOAD:0000000000000100 dq 1000h ; Physical address
LOAD:0000000000000108 dq 191h ; Size in file image
LOAD:0000000000000110 dq 191h ; Size in memory image
LOAD:0000000000000118 dq 1000h ; Alignment
[ ... ] skipping staff
The IDA view shows the virtual memory layout as if it were loaded into memory with base_addr
= 0x0000
and each program header contains metadata for one segment—including its type - Type
, alignment boundary for segment- Alignment
, access permissions - Flags
, disk location - File offset
, and runtime address - Virtual address
.
These program headers (the binary data embedded from C structures) will be parsed by readelf
. For the sake of illustrating the relation between each program header and associated segment, I’ve added an Idx
column to Program Headers
output below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ readelf -l test
[ ... ]
Program Headers:
Idx Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
00 PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8
01 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
02 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000628 0x0000000000000628 R 0x1000
03 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000191 0x0000000000000191 R E 0x1000
04 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000114 0x0000000000000114 R 0x1000
05 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000258 0x0000000000000260 RW 0x1000
06 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8
07 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000030 0x0000000000000030 R 0x8
08 NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368 0x0000000000000044 0x0000000000000044 R 0x4
09 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000030 0x0000000000000030 R 0x8
10 GNU_EH_FRAME 0x0000000000002034 0x0000000000002034 0x0000000000002034 0x0000000000000034 0x0000000000000034 R 0x4
11 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
12 GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000248 0x0000000000000248 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
Our focus is on the LOAD
segments which have the p_type
= PT_LOAD (1)
. These are the segments that will actually get loaded into memory2. You are probably familiar with some of the sections—.text
(RE) section in seg03
, .rodata
(R) section in seg04
, .data
and .bss
(RW) sections in seg05
. The primary distinction you will see compared to other segments is the value of Align
; Unlike other segments, the LOAD
segments always have an Align
value of 0x1000
.
Page alignment and its mapping
The p_align
member of Phdr
tells the ELF loader3, “When you are mapping each segment of ELF file to memory, please let the base address of these segments be a multiple of 0x1000
4”. In other words, loadable segments are supposed to be page aligned.
The basic mapping unit is a page, with a size of 0x1000
on x86-64 GNU/Linux. We’ll draw a memory layout diagram for the ELF file based on readelf
output2:
The loader will map each file page (a page-sized data block from the ELF file) into virtual memory by means of mmap()
4 which requires the file offset to be a multiple of page size; The loader sequentially looks through the program headers and maps the corresponding segments into memory. Here’s the tricky part: as you see, seg05
is not page aligned. How will the loader deal with it?
First see the result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x555555554000 0x555555555000 r--p 1000 0 test
0x555555555000 0x555555556000 r-xp 1000 1000 test
0x555555556000 0x555555557000 r--p 1000 2000 test
0x555555557000 0x555555558000 r--p 1000 2000 test
0x555555558000 0x555555559000 rw-p 1000 3000 test
0x7ffff7da4000 0x7ffff7dcc000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dcc000 0x7ffff7f54000 r-xp 188000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f54000 0x7ffff7fa3000 r--p 4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa3000 0x7ffff7fa7000 r--p 4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa7000 0x7ffff7fa9000 rw-p 2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 r-xp 2b000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
Focusing on offset
column, the file offset 0x2000
gets mapped twice? Does this result in memory waste, and why?
Addressing the first question. ELF loader sequentially scans through program headers.
It sees the first loadable segment:
- The
seg02
is page-aligned. seg02
ranges0x0000
-0x0628
, within0x0000
-0x1000
; I only need to map 1 file page.- What I should do is to directly map the file page (
0x0000
-0x1000
) into the memory.
Then the second loadable segment:
- The
seg03
is page-aligned. seg03
ranges0x1000
-0x1191
, within0x1000
-0x2000
; I only need to map 1 file page.- What I should do is to directly map the file page (
0x1000
-0x2000
) into the memory.
And the third:
- The
seg04
is page-aligned. seg04
ranges0x2000
-0x2114
, within0x2000
-0x3000
; I only need to map 1 file page.- What I should do is to directly map the file page (
0x2000
-0x3000
) into the memory.
The tricky part is seg05
:
- Its memory range in the file (
0x2db8
-0x3010
) is not page-aligned. - Again,
mmap()
can only map a file page whose offset equals a multiple of the page-size. - So I decided first to map one file page the ranging
0x2000
-0x3000
then the following file page ranging0x3000
-0x4000
, 2 file pages in total.
Thus, here we are! The file page at offset 0x2000
gets mapped twice, leaving the virtual memory from 0x2114
to 0x3db8
unused.
Will it cause memory waste? No, these 2 pages (0x2000
- 0x4000
in virtual memory) share the same physical memory by virtue of demand paging implemented by mmap()
.
Side note #1: Why can’t seg05 be page-aligned?
Scrolling back to the diagram of ELF file memory layout on disk, seg05
mainly contains two parts:
- Offset:
0x2db8
-0x3000
:.init_array
.fini_array
.dynamic
.got
with permissionR
. - Offset:
0x3000
-0x3010
:.data
,.bss
with permissionRW
Obviously, these two parts require different permissions (R
vs RW
). Since each page can have only one permission setting, seg05
spans across two pages with different permission settings.
Side note #2: Why do FileSiz
and MemSiz
of seg05 differ?
1
2
FileSiz MemSiz
0x0000000000000258 0x0000000000000260
When loaded, the size of seg05
got increased by 8 bytes. The uninitialized data section .bss
also has 8 bytes and it won’t take up any space in file until it gets loaded into memory:
1
2
3
4
$ readelf -S test # examine the section headers
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[25] .data PROGBITS 0000000000004000 00003000 0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004010 00003010 0000000000000008 0000000000000000 WA 0 0 1
1
2
3
4
5
6
pwndbg> x/24xb 0x555555558000
<.data>:
0x555555558000: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x555555558008: 0x08 0x80 0x55 0x55 0x55 0x55 0x00 0x00
<.bss>:
0x555555558010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # initialized to zero
Side note #3: WTF is the output of readelf
?
1
2
3
4
5
6
7
Section to Segment mapping:
Segment Sections...
[ ... ]
05(RW) .init_array .fini_array .dynamic .got .data .bss (p_type = PT_LOAD)
06(RW) .dynamic (p_type = PT_DYNAMIC)
[ ... ]
12(R) .init_array .fini_array .dynamic .got (p_type = PT_GNU_RELRO)
Leaving the seg06
aside for now5, why do the permission setting of seg05
and seg12
differ? Things may become clearer if we examine these sections one by one:
1
2
.init_array:0000000000003DB8 __frame_dummy_init_array_entry dq offset frame_dummy
.fini_array:0000000000003DC0 __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ readelf -d test
Dynamic section at offset 0x2dc8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1184
0x0000000000000019 (INIT_ARRAY) 0x3db8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3dc0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3b0
0x0000000000000005 (STRTAB) 0x480
0x0000000000000006 (SYMTAB) 0x3d8
0x000000000000000a (STRSZ) 143 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3fb8
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x610
0x0000000000000007 (RELA) 0x550
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x520
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x510
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
1
2
3
4
5
6
7
8
9
.got:0000000000003FB8 dq offset _DYNAMIC
.got:0000000000003FC0 dq 0
.got:0000000000003FC8 dq 0
.got:0000000000003FD0 dq offset printf
.got:0000000000003FD8 dq offset __libc_start_main
.got:0000000000003FE0 dq offset _ITM_deregisterTMCloneTable
.got:0000000000003FE8 dq offset __gmon_start__
.got:0000000000003FF0 dq offset _ITM_registerTMCloneTable
.got:0000000000003FF8 dq offset __imp___cxa_finalize
All these sections need to be patched at load time.
.init_array
and.fini_array
are arrays of function pointers, which are initially set to relative virtual addresses; These functions will run before and aftermain()
. The absolute virtual addresses will be resolved and replace the original addresses when loaded..dynamic
section is the same as the two above. The relative address will be resolved and patched into absolute address.- The program uses eager binding instead of lazy binding, so the
.got
section needs to be patched first and then restricted toR
permission by the dynamic linker (ld.so
)3, same as the other segments.
Let’s do some examinations to verify:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# .init_array and .fini_array
.init_array:0000000000003DB8 40 11 00 00 00 00 00 00 __frame_dummy_init_array_entry dq offset frame_dummy
.fini_array:0000000000003DC0 00 11 00 00 00 00 00 00 __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux
pwndbg> x/16b 0x0000000000003DB8 + 0x555555554000
0x555555557db8: 0x40 0x51 0x55 0x55 0x55 0x55 0x00 0x00
0x555555557dc0: 0x00 0x51 0x55 0x55 0x55 0x55 0x00 0x00
# .dynamic
LOAD:0000000000003E48 05 00 00 00 00 00 00 00 80 04 00 00 00 00 00 00 Elf64_Dyn <5, 480h> ; DT_STRTAB
LOAD:0000000000003E58 06 00 00 00 00 00 00 00 D8 03 00 00 00 00 00 00 Elf64_Dyn <6, 3D8h> ; DT_SYMTAB
pwndbg> x/32b 0x0000000000003E48 + 0x555555554000
0x555555557e48: 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x555555557e50: 0x80 0x44 0x55 0x55 0x55 0x55 0x00 0x00
0x555555557e58: 0x06 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x555555557e60: 0xd8 0x43 0x55 0x55 0x55 0x55 0x00 0x00
These addresses actually get patched at load-time!
References
- https://blog.zolutal.io/understanding-paging
- https://www.geeksforgeeks.org/operating-systems/paging-in-operating-system
- https://www.geeksforgeeks.org/c/structure-member-alignment-padding-and-data-packing
- https://gist.github.com/x0nu11byt3/bcb35c3de461e5fb66173071a2379779
- https://github.com/compilepeace/BINARY_DISSECTION_COURSE
- https://stackoverflow.com/questions/10410785/where-should-the-bss-section-of-elf-file-take-in-memory
- https://www.gabriel.urdhr.fr/2015/01/22/elf-linking/#dynamic-binary-mapping
- https://stackoverflow.com/questions/38857741/is-dynamic-linker-part-of-kernel-or-gcc-library-on-linux-systems
- https://unix.stackexchange.com/questions/611733/what-is-the-linker-and-what-is-the-loader-in-dynamic-linking
Footnotes
Just copying and pasting the high-level C structure of program header (
Phdr
) to explain how it’s embedded and hard-coded in the program—and what it contains—is not enough. Instead, compare the C structure (fromman elf
), IDA view, andreadelf
output provided in the article for clearer understanding. ↩︎The segments other than
PT_LOAD
have an obvious overlap in sections (e.g., sections fromseg12
(R) are a subset of sections fromseg05
(RW)). These segments (or more accurately, the overlapped sections) will also get loaded. Additional note: Overlapping types like PT_GNU_RELRO are hints for the dynamic linker to post-process, so the loading sequence is that ELF loader of the kernel maps PT_LOAD, then user-space linker handles the rest. ↩︎ ↩︎2There are some differences between ELF loader and dynamic linker. ↩︎ ↩︎2
ELF loader uses
mmap()
to load pages; it requires the base address of the file page it maps to be a multiple of the page size. ↩︎ ↩︎2Why is there a dedicated program header for
.dynamic
? Fromreadelf -d
output, the relative virtual addresses of.init_array
,.fini_array
and.got
are included; It acts as the “headquarters” or “container” for load-time writable sections’ addresses, so a dedicated header lets the linker efficiently locate.dynamic
’s virtual address via the program headers. ↩︎