您的发行版配置了 gcc--enable-default-pie
,因此默认情况下它会生成位置无关的可执行文件(允许可执行文件和库的 ASLR)。如今,大多数发行版都在这样做。
你其实are制作共享对象:PIE 可执行文件是一种使用带有入口点的共享对象的 hack。动态链接器已经支持这一点,并且 ASLR 非常适合安全性,因此这是为可执行文件实现 ASLR 的最简单方法。
32-bit absolute relocations aren't allowed in an ELF shared object; that would stop them from being loaded outside the low 2GiB (for sign-extended 32-bit addresses). 64-bit absolute addresses are allowed, but generally you only want that for jump tables or other static data, not as part of instructions.1
The recompile with -fPIC
部分错误消息对于手写 asm 来说是伪造的;它是为人们编译的情况而写的gcc -c
然后尝试链接gcc -shared -o foo.so *.o
,与 gcc 其中-fPIE
is not默认值。错误消息可能应该更改,因为许多人在链接手写汇编时遇到此错误。
正确的解读方式recompile with -fPIC
is "制作与 PIE 兼容的新 asm",或者使用 C 源代码的编译器,或者如果您的源代码是 asm,则使用手动编译器。
如何使用 RIP 相对寻址:基础知识
对于没有任何缺点的简单情况,始终使用 RIP 相对寻址。另请参阅下面的脚注 1 和这个语法的答案 https://stackoverflow.com/questions/54745872/how-do-rip-relative-variable-references-like-rip-a-in-x86-64-gas-intel-sy。仅当绝对寻址实际上对代码大小有帮助而不是有害时才考虑使用绝对寻址。例如NASM default rel
在你的文件的顶部。
美国电话电报公司foo(%rip)
或在气体中.intel_syntax noprefix
use [rip + foo]
.
禁用 PIE 模式以使 32 位绝对寻址工作
Use gcc -fno-pie -no-pie
将其覆盖回旧行为。 -no-pie
是链接器选项,-fno-pie是代码生成选项 https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html。仅与-fno-pie
, gcc 将使代码如下mov eax, offset .LC0
不与仍然启用的-pie
.
(clang可以默认启用 PIE,以便:使用clang -fno-pie -nopie
. A 2017年7月补丁 https://reviews.llvm.org/D35462 made -no-pie
的别名-nopie
,为了与gcc兼容,但clang4.0.1没有。)
64 位(次要)或 32 位代码(主要)的 PIE 性能成本
仅与-no-pie
,(但仍然-fpie
)编译器生成的代码(来自 C 或 C++ 源代码)将比必要的速度稍慢且稍大,但仍会链接到位置相关的可执行文件,该可执行文件不会从 ASLR 中受益。“太多的 PIE 不利于性能”据报告,SPEC CPU2006 上的 x86-64 平均速度下降 3% https://security.stackexchange.com/questions/41697/why-doesnt-linux-randomize-the-address-of-the-executable-code-segment/42641#42641(我没有该论文的副本,因此不知道该硬件是什么:/)。但在 32 位代码中,平均速度减慢为 10%,最坏情况为 25%(在 SPEC CPU2006 上)。
PIE 可执行文件的惩罚主要是针对诸如索引静态数组之类的内容,正如 Agner 在问题中所描述的那样,其中使用静态地址作为 32 位立即数或作为[disp32 + index*4]
与 RIP 相关的 LEA 相比,寻址模式可以保存指令和寄存器以将地址存入寄存器。也是5字节mov r32, imm32
而不是 7 字节lea r64, [rel symbol]
将静态地址放入寄存器对于将字符串文字或其他静态数据的地址传递给函数来说是很好的。
-fPIE
仍然假设全局变量/函数没有符号插入,这与-fPIC
对于必须通过 GOT 访问全局变量的共享库(这是使用的另一个原因static
对于任何可以限制在文件范围而不是全局范围的变量)。看Linux 上动态库的令人遗憾的现状 https://web.archive.org/web/20171111043629/http://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/.
Thus -fPIE
比-fPIC
对于 64 位代码,但仍然对于 32 位不利,因为 RIP 相对寻址不可用. See Godbolt 编译器资源管理器上的一些示例 https://gcc.godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,source:%27%23include+%3Cstdio.h%3E%0A//+godbolt+doesn!%27t+build+gcc+with+--enable-default-pie%0A//+so+-fno-pie+-no-pie+is+the+default.%0A%0A//+-fPIE+mostly+just+costs+you+RIP-relative+LEA%0A//+-fPIC+is+even+more+expensive+than+-fPIE+for+global+vars,%0A//++requiring+a+load+to+get+the+address+of+a+global%0A%0A%0A//__attribute__((weak))+extern+int+ext%3B++//+PIE+can!%27t+assume+its+definition+wins,+so+the+address+isn!%27t+a+link-time+constant+anymore%0A%0A//+overhead+only+with+PIC.++RIP-relative+is+already+good+for+static+data%0A//+static++//+would+remove+PIC+overhead%0A++int+ext+%3D+1%3B%0Aint*+scalar_global(void)+%7B%0A++ext+%3D+123%3B%0A++//return+%26ext%3B++//+minor+PIC/PIE+overhead%0A++return+NULL%3B%0A%7D%0A%0A//+overhead+with+PIE,+more+overhead+with+PIC+(dependent+load)%0A//+no-pic+can+use+the+address+as+a+disp32+for+an+indexed+addressing+mode%0Aint+arr%5B1024%5D%3B%0Aint+fetch(long+idx)+%7B%0A++return+arr%5Bidx%5D%3B%0A%7D%0A%0A//+PIE+and+PIC+both+use+a+RIP-relative+LEA+for+the+string+literal%0A//+instead+of+mov-immediate+(smaller+and+runs+on+more+ports)%0A//+-fno-plt+works+the+same+for+no-pie,+pie,+and+pic%0A//+Even+no-pie+executables+still+call+shared+libs+through+the+PLT+by+default%0Avoid+call_sharedlib(void)+%7B%0A++puts(%22hello%22)%3B%0A%7D%0A//+jmp+puts++turns+into+puts@PLT+when+the+linker+finds%0A//+the+puts+symbol+in+libc.so+instead+of+a+static+library.%0A%27),l:%275%27,n:%270%27,o:%27C%2B%2B+source+%231%27,t:%270%27)),k:34.28344416186805,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((g:!((h:compiler,i:(compiler:g72,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,trim:%271%27),libs:!(),options:%27-xc+-Wall+-O3++-fno-pie+-no-pie%27,source:1),l:%275%27,n:%270%27,o:%27x86-64+gcc+7.2+(Editor+%231,+Compiler+%233)%27,t:%270%27)),header:(),l:%274%27,m:50,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:g72,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,trim:%271%27),libs:!(),options:%27-xc+-std%3Dc99+-O3+-fPIE+-no-pie%27,source:1),l:%275%27,n:%270%27,o:%27x86-64+gcc+7.2+(Editor+%231,+Compiler+%231)%27,t:%270%27)),k:33.18220551902322,l:%274%27,m:50,n:%270%27,o:%27%27,s:0,t:%270%27)),k:33.21998747260075,l:%273%27,n:%270%27,o:%27%27,t:%270%27),(g:!((g:!((h:compiler,i:(compiler:g72,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,trim:%271%27),libs:!(),options:%27-xc+-Wall+-O3++-fPIC%27,source:1),l:%275%27,n:%270%27,o:%27x86-64+gcc+7.2+(Editor+%231,+Compiler+%232)%27,t:%270%27)),header:(),k:32.49656836553122,l:%274%27,m:50,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:g72,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,trim:%271%27),libs:!(),options:%27-xc+-Wall+-O3++-fno-plt+-fPIC%27,source:1),l:%275%27,n:%270%27,o:%27x86-64+gcc+7.2+(Editor+%231,+Compiler+%234)%27,t:%270%27)),header:(),l:%274%27,m:50,n:%270%27,o:%27%27,s:0,t:%270%27)),k:32.49656836553122,l:%273%27,n:%270%27,o:%27%27,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4。一般,-fPIE
在 64 位代码中,性能/代码大小方面的缺点非常小。特定循环的最坏情况可能只有百分之几。但 32 位 PIE 可能会更糟。
都不是-f
代码生成选项在链接时会产生任何影响,
或者组装时.S
手写汇编。gcc -fno-pie -no-pie -O3 main.c nasm_output.o
是您想要两种选择的情况。
检查您的 GCC 配置
如果你的 GCC 是这样配置的,gcc -v |& grep -o -e '[^ ]*pie'
印刷--enable-default-pie
。对此配置选项的支持已添加到 gcc 中2015年初 https://gcc.gnu.org/ml/gcc-patches/2015-01/msg00701.html。 Ubuntu 在 16.10 中启用了它,Debian 大约在同一时间在 gcc 中启用了它6.2.0-7
(导致内核构建错误:https://lkml.org/lkml/2016/10/21/904 https://lkml.org/lkml/2016/10/21/904).
有关的:将压缩的 x86 内核构建为 PIE https://lkml.org/lkml/2016/10/20/141也受到更改后的默认值的影响。
为什么Linux不随机化可执行代码段的地址? https://security.stackexchange.com/questions/41697/why-doesnt-linux-randomize-the-address-of-the-executable-code-segment是一个较老的问题,关于为什么它不是早期的默认设置,或者在全面启用它之前仅对旧版 Ubuntu 上的几个软件包启用。
注意ld
本身并没有改变它的默认值。它仍然可以正常工作(至少在带有 binutils 2.28 的 Arch Linux 上)。变化在于gcc
默认为通过-pie
作为链接器选项,除非您明确使用-static
or -no-pie
.
在 NASM 源文件中,我使用了a32 mov eax, [abs buf]
获得绝对地址。 (我正在测试是否用 6 字节方式编码小绝对地址(地址大小 + mov eax,moffs:67 a1 40 f1 60 00
)在 Intel CPU 上出现 LCP 停顿。It does https://bugs.llvm.org/show_bug.cgi?id=34733#c3.)
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 还可以制作一个“静态 PIE”-static-pie
; ASLRed 没有动态库或 ELF 解释器。不一样的东西-static -pie
- 尽管这些相互冲突(你得到一个静态的非 PIE)它可能会改变 https://gcc.gnu.org/ml/gcc/2018-01/msg00255.html.
有关的:使用/不使用 libc 构建静态/动态可执行文件,定义_start or main https://stackoverflow.com/questions/36861903/assembling-32-bit-binaries-on-a-64-bit-system-gnu-toolchain/36901649.
检查现有可执行文件是否为 PIE
这也被问到:如何测试 Linux 二进制文件是否被编译为位置无关代码? https://unix.stackexchange.com/questions/89211/test-whether-linux-binary-is-compiled-as-position-independent-code
file
and readelf
说 PIE 是“共享对象”,而不是 ELF 可执行文件。 ELF类型的EXEC不能是PIE。
$ 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 -static-pie
是 GCC 默认情况下不做的一件特殊事情,即使-nostdlib
。它显示为LSB pie executable
, dynamically linked
当前版本的file
. (See Linux ldd 中的“静态链接”和“不是动态可执行文件”有什么区别? https://stackoverflow.com/questions/61553723/whats-the-difference-between-statically-linked-and-not-a-dynamic-executable)。它具有ELF型DYN,但是readelf
显示没有.interp
, and ldd
会告诉你它是静态链接的。广东发展银行starti
and /proc/maps
确认执行从其顶部开始_start
,不在 ELF 解释器中。
半相关(但不是真的):另一个最近的 gcc 功能是gcc -fno-plt
。最后调用共享库可以是call [rip + symbol@GOTPCREL]
(美国电话电报公司call *puts@GOTPCREL(%rip)
),没有 PLT 蹦床。
NASM 版本是call [rel puts wrt ..got]
作为替代call puts wrt ..plt
. See 无法在 64 位 Linux 上从汇编 (yasm) 代码调用 C 标准库函数 https://stackoverflow.com/questions/52126328/cant-call-c-standard-library-function-on-64-bit-linux-from-assembly-yasm-code。这适用于 PIE 或非 PIE,并避免链接器为您构建 PLT 存根。
一些发行版已经开始启用它。它还避免了需要可写+可执行的内存页面,因此有利于防止代码注入的安全性。 (我认为现代 PLT 实现也不需要,只需更新 GOT 指针而不是重写jmp rel32
说明,因此可能不存在安全差异。)
对于进行大量共享库调用的程序来说,这是一个显着的加速,例如x86-64clang -O2 -g
在任何硬件上编译tramp3d从41.6秒到36.8秒补丁作者测试过 https://gcc.gnu.org/ml/gcc-patches/2015-05/msg00225.html。 (clang 可能是共享库调用的最坏情况,会对小型 LLVM 库函数进行大量调用。)
它确实需要早期绑定而不是惰性动态链接,因此对于立即退出的大程序来说速度较慢。 (例如。clang --version
或编译hello.c
)。显然,通过预链接可以减少这种减速。
不过,这并不能消除共享库 PIC 代码中外部变量的 GOT 开销。 (请参阅上面的 godbolt 链接)。
脚注 1:64 位绝对
Linux ELF 共享对象实际上允许使用 64 位绝对地址,其中文本重定位 https://blog.flameeyes.eu/2016/01/textrels-text-relocations-and-their-impact-on-hardening-techniques/允许在不同的地址加载(ASLR 和共享库)。这允许你有跳转表section .rodata
, or static const int *foo = &bar;
没有运行时初始化程序。
So mov rdi, qword msg
有效(10 字节的 NASM/YASM 语法mov r64, imm64 http://felixcloutier.com/x86/MOV.html,又名 AT&T 语法movabs
,唯一可以使用 64 位立即数的指令)。但它更大并且通常比lea rdi, [rel msg]
,如果您决定不禁用,则应该使用它-pie
。根据 Sandybridge 系列 CPU 的说法,从 uop 缓存中获取 64 位立即数的速度较慢阿格纳·福格的微建筑 pdf http://agner.org/optimize/。 (是的,问这个问题的是同一个人。:)
您可以使用 NASMdefault rel
而不是在每个中指定它[rel symbol]
寻址模式。也可以看看Mach-O 64 位格式不支持 32 位绝对地址。 NASM 访问阵列 https://stackoverflow.com/questions/47300844/mach-o-64-bit-format-does-not-support-32-bit-absolute-addresses-nasm-accessing有关避免 32 位绝对寻址的更多说明。 OS X 根本无法使用 32 位地址,因此 RIP 相对寻址也是最好的方法。
在位置相关的代码中(-no-pie
),你应该使用mov edi, msg
当您想要寄存器中的地址时; 5字节mov r32, imm32
甚至比 RIP 相关的 LEA 还要小,并且可以运行它的执行端口更多。