如何诊断 GNU ld 链接器行为随时间变化的差异?

How do I diagnose differences in GNU ld linker behaviour over time?

我有一个小的 x86-64 汇编程序,我在 2018 年编译并链接了它。我现在正在尝试重现构建,但在链接时我在最终二进制文件中得到了不同的结果。

这两个文件都是使用以下命令组装和链接的:

$ nasm -f elf64 prng.asm; ld -s -o prng prng.o 

我在2018年创作的原创ELF,取名prng。我今天创建的版本命名为prng2。我已验证中间目标文件 prng.o 是相同的,因此我排除了源代码或 nasm 作为我所看到的差异的原因。下面我展示了 objdump 在每个 ELF 上的输出,旧的和新的:

原文:

$ objdump -x prng

prng:     file format elf64-x86-64
prng
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004000b0

Program Header:
    LOAD off    0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
         filesz 0x0000000000000150 memsz 0x0000000000000150 flags r-x
    LOAD off    0x0000000000000150 vaddr 0x0000000000600150 paddr 0x0000000000600150 align 2**21
         filesz 0x0000000000000008 memsz 0x0000000000000008 flags rw-

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         000000a0  00000000004000b0  00000000004000b0  000000b0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000008  0000000000600150  0000000000600150  00000150  2**2
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
no symbols

最新:

$ objdump -x prng2

prng2:     file format elf64-x86-64
prng2
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x0000000000401000

Program Header:
    LOAD off    0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**12
         filesz 0x00000000000000e8 memsz 0x00000000000000e8 flags r--
    LOAD off    0x0000000000001000 vaddr 0x0000000000401000 paddr 0x0000000000401000 align 2**12
         filesz 0x00000000000000a0 memsz 0x00000000000000a0 flags r-x
    LOAD off    0x0000000000002000 vaddr 0x0000000000402000 paddr 0x0000000000402000 align 2**12
         filesz 0x0000000000000008 memsz 0x0000000000000008 flags rw-

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         000000a0  0000000000401000  0000000000401000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000008  0000000000402000  0000000000402000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
no symbols

我可以看出差异似乎归结于不同的对齐方式。但是,我无法确定导致使用不同对齐方式的原因。

我相信 ld 的版本会在 Ubuntu 的两个版本之间发生变化。在那段时间 ld 的对齐行为是否有可能发生变化,例如使用不同的默认链接描述文件?

或者 CPU 会影响对齐值的选择吗?

为什么现在的程序头是三段,而以前只有两段?

现代 ld.rodata 部分放在单独的 read-without-exec 页面中。这需要将它放在一个单独的 ELF (程序 header 条目,由加载程序读取)。术语:ELF sections 是 Sections 列表中列出的东西,在 Program Header 列表之后。

Older ld.rodata 放入与 .text 相同的段,read-only with exec。这在过去几年内确实发生了变化,比如 2018 年? (我从 2017 年左右开始使用 Arch GNU/Linux,这是一个 rolling-release 发行版,主要使用未修改的上游源代码,并且它在 IIRC 前后的某个时间发生了变化。)

较早的 ld 也有 ELF header 和 .data 的初始值设定项,与 .text 的开头位于同一磁盘页面中。 (对于 .data 和 .text 总和小于 4k 的小文件)。此磁盘页面以两种不同的方式映射:文本段的 Read + Exec,用于代码和 read-only 数据的虚拟地址,数据段的 Read + Write,用于 .data.

注意 0x00000000004000b0 的入口点地址(在 ELF headers + 数据之后从页面开始的一些小偏移量)与 0x0000000000401000 页面对齐新的可执行文件。 对齐磁盘上的数据允许映射到虚拟内存,而不会将任何内容重叠到不需要可执行的可执行段中。其自然结果是 page-aligned 内存地址,但那是 side-effect,不是目标。

您的可执行文件没有 .rodata 部分(您的输入也没有),但是 ELF header 本身仍然映射到具有 LOAD 属性的段中(映射到内存)。

顺便说一句,更喜欢使用 readelf,而不是 objdump 来检查 ELF headers。


此更改不会将常量数据作为“小工具”跳转到 ,从而有助于防止 ROP 和 Spectre 攻击。 (现在大多数程序通过确保 W^X 使 code-injection 不可能,更复杂的攻击必须寻找现有的可执行字节序列。所以下一步加固是尽可能少的页面不需要是。)

它与您 运行 所在的 CPU 或您构建的 CPU 无关。正如@old_timer 指出的那样,您不应期望来自不同版本的工具链的相同二进制文件。出于这个或其他原因,甚至是将工具版本签名嵌入到元数据中某处的工具,这样的默认值更改当然是可能的。 (像 GCC 这样的编译器会这样做,可能 NASM 和 ld 不会。)


您可以从源代码构建旧版本的 GNU binutils,或者从二进制包中获取旧的 ld。

或者编写您自己的链接描述文件,将 .rodata 放在与 .text 相同的程序段中。 (我认为 ld 通过默认链接描述文件工作;如果您可以在旧的 ld 源代码中找到默认链接描述文件,您可能能够将它与您安装的当前 ld 一起使用。)