为什么 linux 内核将我的 RW 段映射为 RWX?

Why does the linux kernel map my RW segment as RWX?

我有一个非常简单的 ELF 可执行文件:

$ 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

现在,根据我对 ELF 工作原理的理解,我希望分为三个部分:

但是,当我实际查看可执行文件 运行 使用 /proc/pid/maps 时得到的内容时,我看到以下内容:

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

这完全不是我所期望的。这是怎么回事?

这里的答案有两个,一部分是动态链接器贡献的,另一部分是内核贡献的。为了看到这一点,让我们在进入动态链接器后立即查看内存映射(例如,通过在 _dl_start 中设置断点)。我们看到:

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

这至少更接近我们想要的(它在正确的位置有正确的片段)。现在,最后一段被拆分的原因是 GNU_RELRO 程序头,它告诉动态链接器 "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(注意它忽略了在程序头,尽管它们似乎通常设置为 PF_R)。

但这只是谜团的一半。我们还有那些讨厌的 PROT_EXEC 位,我们没有订购。这些结果归结为 linux 内核的一个特性,称为 READ_IMPLIES_EXEC 个性,导致所有具有 PROT_READ 权限的地图也具有 PROT_EXEC 权限(参见 man page for personality(2)). It turns out that for compatibility reasons, linux automatically sets this personality 除非 PT_GNU_STACK 程序头 告诉它不要。如果所有输入对象都有一个(空的).note.GNU-stack 部分,链接器会自动创建此程序头。有关该机制的更多信息,请参阅 here