如何诊断 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
我可以看出差异似乎归结于不同的对齐方式。但是,我无法确定导致使用不同对齐方式的原因。
- 我今天使用的是 Ubuntu 20.04.1,而在 2018 年我使用的是 Ubuntu 16.04.
- 我今天使用的是 AMD Ryzen 3700X CPU,而在 2018 年我使用的是英特尔酷睿 i7-860。
我相信 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 一起使用。)
我有一个小的 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
我可以看出差异似乎归结于不同的对齐方式。但是,我无法确定导致使用不同对齐方式的原因。
- 我今天使用的是 Ubuntu 20.04.1,而在 2018 年我使用的是 Ubuntu 16.04.
- 我今天使用的是 AMD Ryzen 3700X CPU,而在 2018 年我使用的是英特尔酷睿 i7-860。
我相信 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 一起使用。)