TL:DR:并非所有绝对地址的使用都会在非 PIE 可执行文件(ELF 类型 EXEC,而不是 DYN)中具有重定位信息。因此内核的程序加载器无法找到它们全部应用修正。
因此,无法为构建为非 PIE 的可执行文件追溯启用 ASLR。传统的可执行文件无法将自己标记为在每次使用绝对地址时都具有重定位元数据,并且添加这样的功能也没有任何意义,因为如果您想要文本 ASLR,您只需构建一个 PIE。
因为保证 ELF 类型的 EXEC Linux 可执行文件被加载/映射到链接器在链接时选择的固定基地址,所以在可执行文件中为内部符号创建符号表条目会浪费空间。所以工具链没有这样做,也没有理由开始。这就是传统 ELF 可执行文件的设计方式;早在 90 年代中期,Linux 就从 a.out 切换到了 ELF,那时堆栈 ASLR 还没有出现,所以它并没有引起人们的注意。
例如static char buf[100] 的绝对地址可能嵌入在使用它的机器代码中的某个位置(如果我们谈论的是 32 位代码,或者将地址放入寄存器的 64 位代码),但没有办法知道在哪里或多少次。
此外,特别是对于 x86-64,非 PIE 可执行文件的默认代码模型保证静态地址(文本/数据/bss)都将位于虚拟地址空间的低 2GiB 中,因此 32 位绝对有符号或未签名的地址可以工作,rel32 位移可以从任何地方到达任何地方。这就是为什么非 PIE 编译器输出使用 mov $symbol, %edi(5 个字节)而不是 lea symbol(%rip), %rdi(7 个字节)将地址放入寄存器中的原因。 https://godbolt.org/z/89PeK1
因此,即使您确实知道每个绝对地址在哪里,您也只能在低 2GiB 中对其进行 ASLR,从而限制您可以引入的熵位数。 (我认为 Windows 有这样的模式:LargeAddressAware = no。但 Linux 没有。32-bit absolute addresses no longer allowed in x86-64 Linux? 同样,PIE 是允许文本 ASLR 的更好方法,所以如果人们(发行版)想要它的好处,就应该为此编译.)
与 Windows 不同,Linux 不会在可以通过从源代码重新编译二进制文件来更好、更高效地处理的事情上花费大量精力。
话虽如此,即使在 PIC / PIE ELF 共享对象中,GNU/Linux 确实 支持 64 位 绝对地址的修复重定位。这就是为什么像 NASM mov rdi, BUFFER 这样的初学者代码甚至可以在共享库中工作的原因:使用 objdump -drwC -Mintel 在 mov reg, imm64 指令中查看有关该符号使用的重定位信息。如果BUFFER 不是全局符号,则lea rdi, [rel BUFFER] 不需要任何重定位条目。 (等效于 C static。)
您可能想知道为什么元数据是必不可少的:
没有可靠的方法在文本/数据中搜索可能的绝对地址;可能会出现误报。例如/usr/bin/ld 可能包含 0x401000 作为 x86-64 可执行文件的默认起始地址。您不希望ld 的代码+数据的 ASLR 也更改其默认值。或者该整数值可能在许多程序中以多种方式出现,例如作为位图。当然,x86-64 机器码是可变长度的,所以在最一般的情况下,甚至没有可靠的方法来区分操作码和立即操作数。
还有潜在的假阴性。 x86 程序不太可能在具有多条指令的寄存器中构造绝对地址,但这当然是可能的。但是在非 x86 代码中,这很常见。
具有固定长度指令的 RISC 机器不能将 32 位地址放入 32 位指令中;就没有其他空间了。因此,要从静态存储中加载,必须将绝对地址拆分为多个指令,例如 MIPS lui $t0, %hi(0x612300) / lw $t1, %lo(0x612300)($t0) 从绝对地址 0x612300 处的静态变量加载。 (asm 源代码中通常会有一个符号名称,但它不会出现在最终链接的二进制文件中,除非它是 .globl,所以我使用数字作为提醒。)这样的指令不必进来对;在后面的指令中,相同的地址的高半部分可以被其他访问相同的数组或结构重用。