TL:DR: 3 个选项:
- 构建一个非 PIE 可执行文件(
gcc -no-pie -fno-pie call-lib.c libcall.o
),因此当您编写时,链接器将透明地为您生成一个 PLT 条目call puts
.
-
call puts wrt ..plt
like gcc -fPIE
会做。
call [rel puts wrt ..got]
like gcc -fno-plt
会做。
后两者将在 PIE 可执行文件或共享库中工作。第三种方式,wrt ..got
,效率稍高一些。
默认情况下,您的 gcc 正在构建 PIE 可执行文件(x86-64 Linux 中不再允许使用 32 位绝对地址? https://stackoverflow.com/questions/43367427/32-bit-absolute-addresses-no-longer-allowed-in-x86-64-linux).
我不知道为什么,但是这样做时链接器不会自动解析call puts
to 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
. The -no-pie
是关键部分:它是链接器选项。您的 YASM 命令无需更改。
当创建传统的位置相关可执行文件时,链接器将puts
调用目标的符号puts@plt
对你来说,因为我们链接的是动态可执行文件(而不是静态链接 libc )gcc -static -fno-pie
,在这种情况下call
可以去directly到 libc 函数。)
无论如何,这就是 gcc 发出的原因call puts@plt
(GAS 语法)编译时-fpie
(桌面上的默认值,但不是https://godbolt.org/ https://godbolt.org/), 只是call puts
编译时使用-fno-pie
.
See @plt在这里是什么意思? https://stackoverflow.com/questions/5469274/what-does-plt-mean-here有关 PLT 的更多信息,以及Linux 上动态库的抱歉状态 https://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/从几年前开始。 (现代的gcc -fno-plt
就像那篇博客文章中的想法之一。)
顺便说一句,更准确/具体的原型可以让 gcc 在调用之前避免将 EAX 归零foo
:
extern void foo();
在C中的意思是extern void foo(...);
你可以将其声明为extern void foo(void);
,这就是()
意思是C++中的。 C++ 不允许函数声明未指定参数。
装配体改进
你也可以把message
in section .rodata
(只读数据,作为文本段的一部分链接)。
您不需要堆栈帧,只需在调用之前将堆栈对齐 16 即可。一个假人push rax
会做的。
或者我们可以尾调用puts
by jumping到它而不是调用它,堆栈位置与进入该函数时相同。不管有没有 PIE,这都可以工作。只需更换call
with jmp
,只要 RSP 指向您自己的返回地址。
如果你想制作 PIE 可执行文件(或共享库),你有两个选择
-
call puts wrt ..plt
- 通过PLT显式调用。
-
call [rel puts wrt ..got]
- 通过 GOT 条目显式地进行间接调用,就像 gcc 的那样-fno-plt
代码生成的风格。 (使用 RIP 相对寻址模式到达 GOT,因此rel
关键词)。
WRT = 相对于。 NASM 手册文件wrt ..plt https://nasm.us/doc/nasmdoc9.html#section-9.2.5,另请参阅第 7.9.3 节:特殊符号和 WRT https://nasm.us/doc/nasmdoc7.html#section-7.9.3.
通常你会使用default rel
在你的文件的顶部,这样你就可以实际使用call [puts wrt ..got]
并且仍然获得 RIP 相对寻址模式。不能在 PIE 或 PIC 代码中使用 32 位绝对寻址模式。
call [puts wrt ..got]
使用动态链接存储在 GOT 中的函数指针汇编为内存间接调用。 (早期绑定,而不是惰性动态链接。)
NASM文件..got
用于获取变量的地址第 9.2.3 节 https://nasm.us/doc/nasmdoc9.html#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
or jmp puts
并让链接器对其进行排序,除非您想要更高效的 no-plt 风格动态链接。但如果您确实选择静态链接 libc,我认为这是直接跳转到 libc 函数的唯一方法。
(我认为非 PIE 静态链接的可能性是why ld
愿意为非 PIE 自动生成 PLT 存根,但不为 PIE 或共享库自动生成 PLT 存根。它要求您说出链接 ELF 共享对象时的含义。)
如果你确实使用过call puts
在 PIE 中(call rel32
),只有当您静态链接位置无关的实现时它才可以工作puts
到你的 PIE 中,所以整个事情是一个可执行文件,它将在运行时加载到随机地址(通过通常的动态链接器机制),但根本不依赖于libc.so.6
当目标在静态链接时出现时,链接器“放松”调用
GAS call *bar@GOTPCREL(%rip)
uses R_X86_64_GOTPCRELX
(放松)
NASM call [rel bar wrt ..got]
uses R_X86_64_GOTPCREL
(不放松)
对于手写汇编来说,这不是一个问题;你可以使用call bar
当您知道该符号将出现在另一个符号中时.o
(而不是.so
)您要链接的内容。但是 C 编译器不知道库函数和您用原型声明的其他用户函数之间的区别(除非您使用类似的东西)gcc -fvisibility=hidden
https://gcc.gnu.org/wiki/Visibility https://gcc.gnu.org/wiki/Visibility或属性/编译指示)。
不过,如果您静态链接库,您可能希望编写链接器可以优化的 asm 源代码,但据我所知,您不能使用 NASM 做到这一点。您可以将符号导出为隐藏(在静态链接时可见,但在最终 .so 中动态链接时不可见):global bar:function hidden
,但那是在定义函数的源文件中,而不是访问它的文件中。
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] 是not放松到e8 rel32
使用气体:
_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>
两个电话都放松为直接e8
call rel32
直接到达目标地址。间接调用中的额外字节填充为67
地址大小前缀(对call rel32
),将指令填充到相同的长度。 (因为现在重新组装和重新计算函数内的所有相关分支以及对齐等已经太晚了。)
这会发生在call *puts@GOTPCREL(%rip)
如果你静态链接 libc,gcc -static
.