我有一个函数foo
,它是用汇编语言编写的,并在64位Linux(Ubuntu)上使用yasm和GCC进行了编译。它只是使用puts()
将消息打印到stdout,如下所示:
bits 64
extern puts
global foo
section .data
message:
db 'foo() called', 0
section .text
foo:
push rbp
mov rbp, rsp
lea rdi, [rel message]
call puts
pop rbp
ret
由使用GCC编译的C程序调用:
extern void foo();
int main() {
foo();
return 0;
}
构建命令:
yasm -f elf64 foo_64_unix.asm
gcc -c foo_main.c -o foo_main.o
gcc foo_64_unix.o foo_main.o -o foo
./foo
问题出在这里
在运行程序时,它会显示一条错误消息,并在调用puts
期间立即出现段错误:
./foo: Symbol `puts' causes overflow in R_X86_64_PC32 relocation
Segmentation fault
在与objdump拆卸后,我看到调用是用错误的地址进行的:
0000000000000660 <foo>:
660: 90 nop
661: 55 push %rbp
662: 48 89 e5 mov %rsp,%rbp
665: 48 8d 3d a4 09 20 00 lea 0x2009a4(%rip),%rdi
66c: e8 00 00 00 00 callq 671 <foo+0x11> <-- here
671: 5d pop %rbp
672: c3 retq
(671是下一条指令的地址,而不是puts
的地址)
但是,如果我用C重写相同的代码,则调用将以不同的方式完成:
645: e8 c6 fe ff ff callq 510 <puts@plt>
即它引用了PLT中的puts
。
是否可以告诉yasm生成类似代码?
答案 0 :(得分:5)
您的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 ^ 32,因此我们收到有关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在使用call puts@plt
(台式机上的默认值,而不是https://godbolt.org/上的默认值)进行编译时发出-fpie
的原因,而在编译时仅call puts
的原因使用-fno-pie
进行编译。
有关PLT的更多信息,请参见What does @plt mean here?。
顺便说一句,更准确/更具体的原型将使gcc避免在调用foo
之前将EAX清零:
extern void foo();
表示extern void foo(...);
您可以将其声明为extern void foo(void);
,这就是()
在C ++中的含义。
asm改进
您还可以将message
放在section .rodata
(只读数据,链接为文本段的一部分)中。
您不需要堆栈框架,只需要在调用之前将堆栈按16对齐即可。虚拟push rax
可以做到。或者我们可以通过跳转而不是调用它来尾随puts
的调用,其栈位置与该函数的入口相同。无论有无PIE,此功能都可以使用。
call puts wrt ..plt
通过PLT明确调用call [puts wrt ..GOTPCREL]
通过GOT条目显式地进行间接调用,就像gcc的-fno-plt
样式的代码生成一样。
; 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:
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
jmp puts WRT ..plt ; tailcall puts
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
jmp [puts wrt ..GOTPCREL]
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
jmp puts
在位置相关的可执行文件中,可以使用mov edi, message
代替相对于RIP的LEA。它的代码较小,可以在大多数CPU的更多执行端口上运行。
在非PIE可执行文件中,您也可以使用jmp puts
并让链接程序对其进行整理,除非您希望使用无plt样式的动态链接。但是,如果您选择静态链接libc,我认为这是将jmp直接连接到libc函数的唯一方法。
答案 1 :(得分:4)
已删除评论的清理版本
IIRC,0xe8
操作码后跟一个有符号的偏移量,该偏移量将应用于PC(到那时已前进到下一条指令)以计算分支目标。因此,objdump将分支目标解释为0x671
。
Yasm正在渲染零,因为它可能已在该偏移量上放置了重定位,这就是它要求加载器在加载期间为puts
填充正确偏移量的方式。加载程序在计算重定位时遇到溢出,这可能表明puts
与您的调用之间的偏移量比32位带符号偏移量所表示的偏移量还大。因此,加载程序无法修正此指令,您会崩溃。
66c: e8 00 00 00 00
显示未填充的地址。如果您在重定位表中查看,应该在0x66d
上看到一个重定位。对于汇编器而言,使用重定位为全零的地址/偏移填充地址并不少见。
This page建议YASM具有WRT
指令,该指令可以控制.got
,.plt
等的使用。
根据this page上的S9.2.5,您可以说CALL puts WRT ..plt
(假定Yasm使用相同的语法,因为这是NASM参考)