提问人:A Fog 提问时间:4/12/2017 最后编辑:Peter CordesA Fog 更新时间:10/9/2023 访问量:18710
x86-64 Linux 中不再允许 32 位绝对地址?
32-bit absolute addresses no longer allowed in x86-64 Linux?
问:
64 位 Linux 默认使用小内存模型,该模型将所有代码和静态数据置于 2GB 地址限制以下。这可确保您可以使用 32 位绝对地址。旧版本的 gcc 对静态数组使用 32 位绝对地址,以节省用于相对地址计算的额外指令。但是,这不再有效。如果我尝试在程序集中创建 32 位绝对地址,则会出现链接器错误: “在创建共享对象时,不能使用针对'.data'的重定位R_X86_64_32S;使用 -fPIC 重新编译”。 当然,此错误消息具有误导性,因为我没有创建共享对象,并且 -fPIC 无济于事。 到目前为止,我发现的是:gcc 版本 4.8.5 对静态数组使用 32 位绝对地址,而 gcc 版本 6.3.0 没有。版本 5 可能也没有。binutils 2.24 中的链接器允许 32 位绝对地址,而 2.28 版则不允许。
此更改的结果是必须重新编译旧库,并且旧汇编代码会损坏。
现在我想问:这个改变是什么时候做出的?它记录在某个地方吗?是否有链接器选项可以使其接受 32 位绝对地址?
答:
您的发行版为 gcc 配置了 ,因此默认情况下它会制作与位置无关的可执行文件(允许可执行文件的 ASLR 以及库)。如今,大多数发行版都在这样做。--enable-default-pie
你实际上是在制作一个共享对象:PIE可执行文件是一种使用带有入口点的共享对象的黑客攻击。动态链接器已经支持此功能,并且 ASLR 对安全性很有帮助,因此这是为可执行文件实现 ASLR 的最简单方法。
ELF 共享对象中不允许 32 位绝对重定位;这将阻止它们在低 2GiB 之外加载(对于符号扩展的 32 位地址)。允许使用 64 位绝对地址,但通常只希望将其用于跳转表或其他静态数据,而不是作为指令的一部分。1
对于手写的 asm,错误消息的部分是伪造的;它是为人们编译然后尝试与 链接的情况而编写的,其中 gcc 不是默认的。错误消息可能应该更改,因为许多人在链接手写的 asm 时会遇到此错误。recompile with -fPIC
gcc -c
gcc -shared -o foo.so *.o
-fPIE
正确的解释方式是“使新的 asm 与 PIE 兼容”,可以使用 C 源代码的编译器,或者如果您的源代码是 asm,则手动进行。recompile with -fPIC
如何使用 RIP 相对寻址:基础知识
对于没有缺点的简单情况,请始终使用 RIP 相对寻址。另请参阅下面的脚注 1 和此语法答案。只有当绝对寻址实际上对代码大小有帮助而不是有害时,才考虑使用绝对寻址。例如,NASM 默认 rel
在文件顶部。
AT&T foo(%rip)
或在 GAS 中使用。.intel_syntax noprefix
[rip + foo]
禁用 PIE 模式以使 32 位绝对寻址工作
使用 gcc -fno-pie -no-pie
将其覆盖回旧行为。 是链接器选项,-fno-pie
是代码生成选项。只有 ,gcc 将使这样的代码不会与仍然启用的 .-no-pie
-fno-pie
mov eax, offset .LC0
-pie
(clang 也可以默认启用 PIE:使用 clang -fno-pie -nopie
。2017 年 7 月的补丁为 ,用于与 gcc 兼容,但 clang4.0.1 没有它。-no-pie
-nopie
64 位(次要)或 32 位代码(主要)的 PIE 性能成本
只有 -no-pie
,(但仍然是 -fpie
)编译器生成的代码(来自 C 或 C++ 源代码)将比必要的速度稍慢和更大,但仍将链接到与位置相关的可执行文件中,这不会从 ASLR 中受益。“过多的 PIE 对性能不利”报告在 SPEC CPU2006 上 x86-64 的平均速度下降了 3%(我没有论文的副本,所以 IDK 它是什么硬件:/)。但在 32 位代码中,平均速度变慢为 10%,最坏情况下为 25%(在 SPEC CPU2006 上)。
PIE 可执行文件的惩罚主要是针对索引静态数组之类的东西,正如 Agner 在问题中描述的那样,其中使用静态地址作为 32 位即时地址或作为寻址模式的一部分可以保存指令和寄存器,而使用 RIP 相对 LEA 将地址放入寄存器中。此外,用于将静态地址获取到寄存器中的 5 字节而不是 7 字节非常适合将字符串文本或其他静态数据的地址传递给函数。[disp32 + index*4]
mov r32, imm32
lea r64, [rel symbol]
-fPIE
仍然假设全局变量/函数没有符号插入,这与共享库不同,共享库必须通过 GOT 才能访问全局变量(这是用于任何可以限制在文件范围而不是全局的变量的另一个原因)。请参阅 Linux 上动态库的糟糕状态。-fPIC
static
因此,它比 64 位代码要糟糕得多,但对于 32 位代码来说仍然很糟糕,因为 RIP 相对寻址不可用。请参阅 Godbolt 编译器资源管理器上的一些示例。平均而言,在 64 位代码中具有非常小的性能/代码大小缺点。对于特定循环,最坏的情况可能只有几个百分点。但 32 位 PIE 可能要糟糕得多。-fPIE
-fPIC
-fPIE
这些代码生成选项在仅链接时都没有任何区别,
或组装手写 ASM 时。 是同时需要两个选项的情况。-f
.S
gcc -fno-pie -no-pie -O3 main.c nasm_output.o
检查 GCC 配置
如果您的 GCC 是这样配置的,gcc -v |& grep -o -e '[^ ]*pie'
会打印 --enable-default-pie
。2015 年初,gcc 中添加了对此配置选项的支持。Ubuntu 在 16.10 中启用了它,而 Debian 在 gcc 中几乎同时启用了它(导致内核构建错误:https://lkml.org/lkml/2016/10/21/904)。6.2.0-7
相关:构建压缩的 x86 内核,因为 PIE 也受到更改的默认值的影响。
为什么 Linux 不随机化可执行代码段的地址?是一个较旧的问题,关于为什么它之前不是默认的,或者在全面启用之前只在较旧的 Ubuntu 上为一些软件包启用。
请注意,ld
本身并没有更改其默认值。它仍然正常工作(至少在带有 binutils 2.28 的 Arch Linux 上)。更改是默认为作为链接器选项传递,除非您显式使用 或 .gcc
-pie
-static
-no-pie
在 NASM 源文件中,我曾经获得一个绝对地址。(我正在测试编码小型绝对地址的 6 字节方式(address-size + mov eax,moffs: )是否在 Intel CPU 上出现 LCP 停滞。确实如此。a32 mov eax, [abs buf]
67 a1 40 f1 60 00
nasm -felf64 -Worphan-labels -g -Fdwarf testloop.asm &&
ld -o testloop testloop.o # works: static executable
gcc -v -nostdlib testloop.o # doesn't work
...
..../collect2 ... -pie ...
/usr/bin/ld: testloop.o: relocation R_X86_64_32 against `.bss' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status
gcc -v -no-pie -nostdlib testloop.o # works
gcc -v -static -nostdlib testloop.o # also works: -static implies -no-pie
GCC 还可以使用 ;没有动态库或 ELF 解释器的 ASLR。不是一回事 - 它们相互冲突(你得到一个静态的非 PIE),尽管它可能会改变。-static-pie
-static -pie
相关:使用/不使用 libc 构建静态/动态可执行文件,定义 _start
或 main
。
检查现有可执行文件是否为 PIE
这个问题也被问到:如何测试Linux二进制文件是否被编译为与位置无关的代码?
file
并说 PIE 是“共享对象”,而不是 ELF 可执行文件。ELF 类型的 EXEC 不能是 PIE。readelf
$ gcc -fno-pie -no-pie -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB executable, ...
$ gcc -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB shared object, ...
## Or with a more recent version of file:
a.out: ELF 64-bit LSB pie executable, ...
gcc 是 GCC 默认不做的特殊事情,即使使用 .它显示为 ,当前版本为 。(参见 Linux ldd 中的“静态链接”和“非动态可执行文件”有什么区别?它具有 ELF 类型的 DYN,但显示没有 ,并会告诉您它是静态链接的。GDB 并确认执行从其顶部开始,而不是在 ELF 解释器中。-static-pie
-nostdlib
LSB pie executable
dynamically linked
file
readelf
.interp
ldd
starti
/proc/maps
_start
半相关(但不是真的):最近的另一个 gcc 功能是 gcc -fno-plt
。最后,对共享库的调用可以只是(AT&T),没有PLT蹦床。call [rip + symbol@GOTPCREL]
call *puts@GOTPCREL(%rip)
此的 NASM 版本是
的替代方法。请参见无法从汇编 (yasm) 代码在 64 位 Linux 上调用 C 标准库函数。这适用于 PIE 或非 PIE,并避免链接器为您构建 PLT 存根。call [rel puts wrt ..got]
call puts wrt ..plt
一些发行版已经开始启用它。它还避免了需要可写 + 可执行内存页,因此有利于防止代码注入的安全性。(我认为现代 PLT 实现也不需要它,只是更新 GOT 指针而不是重写指令,因此可能没有安全差异。jmp rel32
对于进行大量共享库调用的程序来说,这是一个显着的加速,例如,x86-64 编译 tramp3d 在补丁作者测试的任何硬件上从 41.6 秒增加到 36.8 秒。(clang 可能是共享库调用的最坏情况,它对小型 LLVM 库函数进行了大量调用。clang -O2 -g
它确实需要早期绑定而不是延迟动态链接,因此对于立即退出的大型程序来说,它的速度较慢。(例如 或编译 )。显然,这种放缓可以通过预链接来减少。clang --version
hello.c
但是,这并不能消除共享库 PIC 代码中外部变量的 GOT 开销。(请参阅上面的 godbolt 链接)。
脚注 1:64 位绝对值
Linux ELF 共享对象中实际上允许使用 64 位绝对地址,文本重定位允许在不同的地址(ASLR 和共享库)加载。这允许您在 中或没有运行时初始值设定项的情况下跳转表。section .rodata
static const int *foo = &bar;
所以有效(10字节mov r64,imm64
的NASM/YASM语法,又名AT&T语法,唯一可以使用64位即时指令)。但是它比 lea rdi 更大,通常更慢,[rel msg]
,如果您决定不禁用,您应该使用它。根据 Agner Fog 的 microarch pdf,从 Sandybridge 系列 CPU 上的 uop 缓存中获取 64 位即时版本的速度较慢。(是的,问这个问题的同一个人:)。mov rdi, qword msg
movabs
-pie
您可以使用 NASM,而不是在每个寻址模式下指定它。另请参阅 Mach-O 64 位格式不支持 32 位绝对地址。NASM 访问阵列,了解有关避免 32 位绝对寻址的更多描述。OS X 根本不能使用 32 位地址,因此 RIP 相对寻址也是最好的方法。default rel
[rel symbol]
在位置相关代码 (-no-pie
) 中,当您希望在寄存器中获取地址时,应使用 mov edi、msg
;5 字节甚至比 RIP 相对 LEA 还要小,更多的执行端口可以运行它。mov r32, imm32
评论
-m32
-m32
-fPIE
-pie
-fPIE
static int *foo = &bar
mov r64, imm64
-fno-plt
评论