TL:DR:3 个选项:
- 构建非 PIE 可执行文件 (
gcc -no-pie -fno-pie call-lib.c libcall.o),以便在您编写 call puts 时,链接器会透明地为您生成 PLT 条目。
-
call puts wrt ..plt 就像 gcc -fPIE 一样。
call [rel puts wrt ..got] 就像gcc -fno-plt 一样。
后两者将在 PIE 可执行文件或共享库中工作。第三种方式,wrt ..got,效率略高。
默认情况下,您的 gcc 正在构建 PIE 可执行文件 (32-bit absolute addresses no longer allowed in x86-64 Linux?)。
我不知道为什么,但是这样做时链接器不会自动将call puts 解析为call puts@plt。仍然有一个puts PLT 条目生成,但call 没有去那里。
在运行时,动态链接器尝试将puts 直接解析为该名称的libc 符号并修复call rel32。但是符号距离超过 +-2^31,所以我们会收到关于 R_X86_64_PC32 重定位溢出的警告。目标地址的低 32 位是正确的,但高位不正确。 (因此您的 call 会跳转到错误的地址)。
如果我使用 gcc -no-pie -fno-pie call-lib.c libcall.o 构建,您的代码对我有用。 -no-pie 是关键部分:它是链接器选项。您的 YASM 命令不必更改。
在制作传统的位置相关可执行文件时,链接器会将调用目标的 puts 符号为您转换为 puts@plt,因为我们正在链接动态可执行文件(而不是静态链接 libc 与 gcc -static -fno-pie,在这种情况下,call 可以直接转到 libc 函数。)
无论如何,这就是为什么 gcc 在使用 -fpie(桌面上的默认值,但不是 https://godbolt.org/ 上的默认值)编译时发出 call puts@plt(GAS 语法),但在使用 @987654354 编译时只是 call puts @。
有关 PLT 的更多信息,请参阅 What does @plt mean here?,以及几年前的 Sorry state of dynamic libraries on Linux。 (现代的gcc -fno-plt 就像那篇博文中的想法之一。)
顺便说一句,更准确/更具体的原型可以让 gcc 在调用 foo 之前避免将 EAX 归零:
extern void foo(); 在 C 中的意思是 extern void foo(...);
您可以将其声明为extern void foo(void);,这就是() 在C++ 中的含义。 C++ 不允许函数声明未指定 args。
asm 改进
您也可以将message 放入section .rodata(只读数据,作为文本段的一部分链接)。
您不需要堆栈帧,只需在调用前将堆栈对齐 16 即可。一个虚拟的push rax 会做到这一点。
或者我们可以通过 跳转 对 puts 进行尾调用,而不是调用它,堆栈位置与进入此函数时相同。无论有没有 PIE,这都适用。只需将call 替换为jmp,只要RSP 指向您自己的返回地址即可。
如果你想制作 PIE 可执行文件(或共享库),你有两个选择
-
call puts wrt ..plt - 通过 PLT 显式调用。
-
call [rel puts wrt ..got] - 通过 GOT 条目显式地进行间接调用,例如 gcc 的 -fno-plt 代码生成样式。 (使用 RIP 相对寻址模式来访问 GOT,因此使用了 rel 关键字)。
WRT = 尊重。 NASM 手册documents wrt ..plt,另见section 7.9.3: special symbols and WRT。
通常您会在文件顶部使用default rel,因此您实际上可以使用call [puts wrt ..got] 并且仍然获得相对于RIP 的寻址模式。您不能在 PIE 或 PIC 代码中使用 32 位绝对寻址模式。
call [puts wrt ..got] 使用动态链接存储在 GOT 中的函数指针汇编为内存间接调用。 (早期绑定,而不是惰性动态链接。)
NASM 文档..got 用于获取section 9.2.3 中变量的地址。 (其他)库中的函数是相同的:您从 GOT 获取指针而不是直接调用,因为偏移量不是链接时间常量,可能不适合 32 位。
YASM 也接受 call [puts wrt ..GOTPCREL],类似于 AT&T 语法 call *puts@GOTPCREL(%rip),但 NASM 不接受。
; don't use BITS 64. You *want* an error if you try to assemble this into a 32-bit .o
default rel ; RIP-relative addressing instead of 32-bit absolute by default; makes the [rel ...] optional
section .rodata ; .rodata is best for constants, not .data
message:
db 'foo() called', 0
section .text
global foo
foo:
sub rsp, 8 ; align the stack by 16
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
call puts WRT ..plt ; tailcall puts
;or
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
call [rel puts wrt ..got]
;or
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
call puts ; linker will rewrite it into call puts@plt
add rsp,8 ; restore the stack, undoing the add
ret
在位置-依赖 Linux 可执行文件中,您可以使用mov edi, message 而不是相对于 RIP 的 LEA。它的代码量更小,可以在大多数 CPU 上的更多执行端口上运行。 (有趣的事实:MacOS 总是将“图像库”放在低 4GiB 之外,因此无法进行这种优化。)
在非 PIE 可执行文件中,您也可以使用 call puts 或 jmp puts 并让链接器对其进行排序,除非您想要更有效的非 plt 样式动态链接。但是,如果您确实选择静态链接 libc,我认为这是您直接 jmp 到 libc 函数的唯一方法。
(我认为非 PIE 的静态链接的可能性是 为什么 ld 愿意为非 PIE 自动生成 PLT 存根,而不是为 PIE 或共享库。它需要你在链接 ELF 共享对象时说出你的意思。)
如果您确实在 PIE (call rel32) 中使用了 call puts,那么只有在您将与位置无关的 puts 实现静态链接到您的 PIE 时它才能工作,所以整个事情就是一个可执行文件在运行时在随机地址加载(通过通常的动态链接器机制),但根本不依赖于libc.so.6
当目标在静态链接时存在时,链接器“放松”调用
GAS call *bar@GOTPCREL(%rip) 使用 R_X86_64_GOTPCRELX(可放松)
NASM call [rel bar wrt ..got] 使用 R_X86_64_GOTPCREL(不可放松)
这对于手写 asm 来说问题不大;当您知道符号将出现在您要链接的另一个.o(而不是.so)中时,您可以只使用call bar。但是 C 编译器不知道库函数和您使用原型声明的其他用户函数之间的区别(除非您使用诸如 gcc -fvisibility=hidden https://gcc.gnu.org/wiki/Visibility 或属性/编译指示之类的东西)。
不过,如果您静态链接库,您可能希望编写链接器可以优化的 asm 源代码,但 AFAIK 无法使用 NASM 来做到这一点。您可以使用global bar:function hidden 将符号导出为隐藏(在静态链接时可见,但对于最终 .so 中的动态链接不可见),但这是在定义函数的源文件中,而不是在访问它的文件中。
global bar
bar:
mov eax,231
syscall
call bar wrt ..plt
call [rel bar wrt ..got]
extern bar
第二个文件,在用nasm -felf64 组装并用objdump -drwc -Mintel 反汇编后查看重定位:
0000000000000000 <.text>:
0: e8 00 00 00 00 call 0x5 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # 0xb 7: R_X86_64_GOTPCREL bar-0x4
与ld (GNU Binutils) 2.35.1 链接后 - ld bar.o bar2.o -o bar
0000000000401000 <_start>:
401000: e8 0b 00 00 00 call 401010 <bar>
401005: ff 15 ed 1f 00 00 call QWORD PTR [rip+0x1fed] # 402ff8 <.got>
40100b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
0000000000401010 <bar>:
401010: b8 e7 00 00 00 mov eax,0xe7
401015: 0f 05 syscall
请注意,PLT 表单已放松为直接call bar,PLT 已消除。但是ff 15 调用 [rel mem] 没有放松为e8 rel32
使用 GAS:
_start:
call bar@plt
call *bar@GOTPCREL(%rip)
gcc -c foo.s && disas foo.o
0000000000000000 <_start>:
0: e8 00 00 00 00 call 5 <_start+0x5> 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # b <_start+0xb> 7: R_X86_64_GOTPCRELX bar-0x4
注意 R_X86_64_GOTPCRELX 末尾的 X。
ld bar2.o foo.o -o bar && disas bar:
0000000000401000 <bar>:
401000: b8 e7 00 00 00 mov eax,0xe7
401005: 0f 05 syscall
0000000000401007 <_start>:
401007: e8 f4 ff ff ff call 401000 <bar>
40100c: 67 e8 ee ff ff ff addr32 call 401000 <bar>
这两个电话都放松了直接e8call rel32直接到目标地址。间接调用中的额外字节用67 地址大小前缀填充(对call rel32 没有影响),将指令填充到相同的长度。 (因为重新组装和重新计算函数内的所有相关分支,对齐等已经太迟了。)
如果您将 libc 与 gcc -static 静态链接,call *puts@GOTPCREL(%rip) 会发生这种情况。