I have a very simple ELF executable:
$ readelf -l ./plt.out
Elf file type is EXEC (Executable file)
Entry point 0x400338
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x00000000003ff040 0x00000000003ff040
0x0000000000000188 0x0000000000000188 R E 8
LOAD 0x0000000000000000 0x00000000003ff000 0x00000000003ff000
0x0000000000001000 0x0000000000001000 RW 1000
INTERP 0x00000000000001c8 0x00000000003ff1c8 0x00000000003ff1c8
0x0000000000000032 0x0000000000000032 R 1
[Requesting program interpreter: /data/keno/new_glibc/usr/lib/ld-linux-x86-64.so.2]
LOAD 0x0000000000001000 0x0000000000400000 0x0000000000400000
0x00000000000003b0 0x00000000000003b0 R E 1000
LOAD 0x0000000000001ea0 0x0000000000600ea0 0x0000000000600ea0
0x0000000000000180 0x0000000000000180 RW 1000
DYNAMIC 0x0000000000001ea0 0x0000000000600ea0 0x0000000000600ea0
0x0000000000000150 0x0000000000000150 RW 8
GNU_RELRO 0x0000000000001ea0 0x0000000000600ea0 0x0000000000600ea0
0x0000000000000160 0x0000000000000160 R 1
Now, from my understanding of how ELF works, I would expect three segments:
0x3ff000-0x400000
0x400000-0x401000
0x600000-0x602000
(0xea0+0x180 > 0x1000
)However, when I actually look at what I get while the executable is running using /proc/pid/maps
, I see the following:
003ff000-00400000 rwxp 00000000 00:28 1456774 plt.out
00400000-00401000 r-xp 00001000 00:28 1456774 plt.out
00600000-00601000 r-xp 00001000 00:28 1456774 plt.out
00601000-00602000 rwxp 00002000 00:28 1456774 plt.out
which is not at all what I expected. What's going on here?
The answer here is two-fold, one part being contributed by the dynamic linker, the other by the kernel. To see, this, let us look at the memory map right after entering the dynamic linker (e.g. by setting a breakpoint in _dl_start). We see:
003ff000-00400000 rwxp 00000000 00:28 1456774 plt.out
00400000-00401000 r-xp 00001000 00:28 1456774 plt.out
00600000-00602000 rwxp 00001000 00:28 1456774 plt.out
which is at least closer to what we wanted (it has the correct segments, in the right places). Now, the reason the last segment gets split up is because of the GNU_RELRO program header, which says to the dynamic linker "Hey, I'm not gonna need to write to this anymore after you're done with your initial relocations", so the dynamic linker faithfully tries to set that region of memory to PROT_READ (note it ignores the actual permission flags set in the program header, though they appear to be conventionally set to PF_R
).
That's only half the mystery though. We still have those pesky PROT_EXEC bits left that we didn't order. Those turn out to come down to a feature of the linux kernel called the READ_IMPLIES_EXEC personality, causing all maps with PROT_READ permission to also have PROT_EXEC permission (see the man page for personality(2)). It turns out that for compatibility reasons, linux automatically sets this personality unless a PT_GNU_STACK program header
tells it not to. The linker automatically creates this program header, if all input objects have an (empty) .note.GNU-stack
section. See here for more information on that mechanism.