Skip to content
Advertisement

Overlapping mappings for loaded ELF segments

I’d like to understand a detail of how the dynamic loader creates mappings for ELF segments.

Consider a tiny shared library linked with GNU ld. The program headers are:

Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD           0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW  0x200000
DYNAMIC        0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW  0x8
GNU_EH_FRAME   0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R   0x4
GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
GNU_RELRO      0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R   0x1

This shared object can print the mappings of the process it is loaded in (/proc/self/maps), snippet:

7fd1f057b000-7fd1f057c000 r-xp 00000000 fe:00 12090538    /path/libmy.so
7fd1f057c000-7fd1f077b000 ---p 00001000 fe:00 12090538    /path/libmy.so
7fd1f077b000-7fd1f077c000 r--p 00000000 fe:00 12090538    /path/libmy.so
7fd1f077c000-7fd1f077d000 rw-p 00001000 fe:00 12090538    /path/libmy.so

If I print the address of a mutable global variable, the printed address is in the fourth mapping.

  1. What is the purpose of each of those four mappings?
  2. Why does the dynamic loader create a “padding” mapping with no permissions?

Deconstructing the mappings:

Base address == 7fd1f057b000
Mapping 1: virtual offset 0x000000, size 0x001000, R-X, from file offset 0x0000
Mapping 2: virtual offset 0x001000, size 0x1ff000, ---, from file offset 0x1000
Mapping 3: virtual offset 0x200000, size 0x001000, R--, from file offset 0x0000
Mapping 4: virtual offset 0x201000, size 0x001000, RW-, from file offset 0x1000

My current understanding:

Ad 1.

  1. The first mapping is the “text” segment (first program header).
  2. The second mapping looks like a form of padding (no permissions and size such that next segment has a virtual offset of 0x200000).
  3. ???
  4. The “data” segment. Except I’d say it should start at file offset 0 and be size 0x2000. The segment starts at 0xdf8 in the file, which is still on the first page. Also, how can the file size of the “text” segment be larger than the offset of the “data” segment?

Ad 2. Couldn’t the linker just request a mapping at the exact virtual address 7fd1f077b000, creating a hole? Why bother with this mapping?


$ readelf -d libmy.so

Dynamic section at offset 0xe08 contains 25 entries:
Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x000000000000000c (INIT)               0x5a8
0x000000000000000d (FINI)               0x848
0x0000000000000019 (INIT_ARRAY)         0x200df8
0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
0x000000000000001a (FINI_ARRAY)         0x200e00
0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
0x0000000000000004 (HASH)               0x190
0x000000006ffffef5 (GNU_HASH)           0x1e0
0x0000000000000005 (STRTAB)             0x380
0x0000000000000006 (SYMTAB)             0x218
0x000000000000000a (STRSZ)              172 (bytes)
0x000000000000000b (SYMENT)             24 (bytes)
0x0000000000000003 (PLTGOT)             0x201000
0x0000000000000002 (PLTRELSZ)           120 (bytes)
0x0000000000000014 (PLTREL)             RELA
0x0000000000000017 (JMPREL)             0x530
0x0000000000000007 (RELA)               0x470
0x0000000000000008 (RELASZ)             192 (bytes)
0x0000000000000009 (RELAENT)            24 (bytes)
0x000000006ffffffe (VERNEED)            0x450
0x000000006fffffff (VERNEEDNUM)         1
0x000000006ffffff0 (VERSYM)             0x42c
0x000000006ffffff9 (RELACOUNT)          3
0x0000000000000000 (NULL)               0x0

$ readelf -Wl libmy.so

Elf file type is DYN (Shared object file)
Entry point 0x630
There are 6 program headers, starting at offset 64

Program Headers:
Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD           0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW  0x200000
DYNAMIC        0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW  0x8
GNU_EH_FRAME   0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R   0x4
GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
GNU_RELRO      0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   01     .init_array .fini_array .dynamic .got .got.plt .data .bss
   02     .dynamic
   03     .eh_frame_hdr
   04
   05     .init_array .fini_array .dynamic .got

Advertisement

Answer

What is the purpose of each of those four mappings?
Why does the dynamic loader create a “padding” mapping with no permissions?

In order to understand the final state, we need to trace through the actions that dynamic linker takes. What are its “instructions”? It needs to load ET_DYN object in memory, at a random address (chosen by the OS). The mappings must satisfy these “commands” (I omitted PhysAddr, since it’s the same as VirtAddr):

       Offset   VirtAddr           FileSiz  MemSiz   Flg Align
LOAD   0x000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD   0x000df8 0x0000000000200df8 0x000250 0x000258 RW  0x200000

Now, the first thing that is important for all ELF binaries, is that in order to work correctly, both LOAD segments must be relocated by the same “base offset”. It would not do to e.g. mmap the first LOAD segment at 0x1000000, and the second at 0x2000000+0x200df8 == 0x2200df8.

For this reason, the dynamic linker (I’ll use rtld contraction for it) must perform the mmap of both segments as a single mmap (otherwise, there is no guarantee that the second mapping will not interfere with something else that is already mapped there). So it performs:

size_t len = 0x200df8 + 0x258;
void *base = mmap(0, len, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);

In your particular case, base == 0x7fd1f057b000, and we have a single mapping, covering both the .text and .data:

7fd1f057b000-7fd1f077d000 r-xp 0 libmy.so

But rtld is far from done. It must now over-mmap the .data (the second) LOAD segment into correct place and with desired permissions (error checking omitted):

  mmap(base + 0x200000, 0xdf8 + 0x258, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);

Our mappings now look like this:

7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so

Next, our file is quite short (less than 4K), and leaving addresses in the range [0x7fd1f057c000, 0x7fd1f077b000) mapped would yield potential for SIGBUS or other confusing errors, when we prefer a simple SIGSEGV.

We could munmap this region, but that disadvantages (some other small library could land in that almost 2MiB region, and confuse other parts of rtld which look for nearest base mapping). Instead, rtld protects that region with no-access, while leaving the mapping intact:

mprotect(0x7fd1f057c000, 0x1ff000, PROT_NONE);

Now our memory map looks almost like the final result you’ve observed:

7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so

But there is one more thing for rtld to do: your object requests (by virtue of having GNU_RELRO segment) that a portion of its writable data be protected from writing after relocation. So rtld performs relocations, and then performs the final mprotect:

mprotect(base + 0x200000, 0xdf8 + 0x208, PROT_READ);

And that results in the final memory map (which matches exactly what you observed):

7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077c000 r--p 0 libmy.so
7fd1f077c000-7fd1f077d000 rw-p 0 libmy.so

I’m having some issues finding documentation on GNU_RELRO.

There is a nice discussion here.

I guess its VirtAddr and FileSize specify which parts should be read-only?

Correct, except it’s the MemSize (but it should always match FileSize).

So the section table is not used?

The section table is never used during dynamic linking, which can work on completely stripped binaries with section table removed. Section table remains in the binary (by default) only to help debugging.

Advertisement