我有一个用汇编编写的函数foo
,并在Linux(Ubuntu)64位上用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生成类似的代码吗?
已删除注释的已清理版本
在IIRC中,0xe8
操作码之后是一个带符号的偏移量,该偏移量将应用于PC(此时已经前进到下一条指令)以计算分支目标。因此,objdump将分支目标解释为0x671
。
Yasm正在渲染零,因为它可能会在该偏移上放置一个重定位,这就是它要求加载器在加载期间为puts
填充正确的偏移量。加载程序在计算reloc时遇到溢出,这可能表示puts
与调用的距离比可以在32位有符号偏移量中表示的距离更远。因此,加载程序无法修复此指令,并且您会崩溃。
66c: e8 00 00 00 00
显示无人居住的地址。如果你查看你的reloc表,你应该在0x66d
上看到一个reloc。汇编程序使用relocs填充地址/偏移量作为全零值并不罕见。
This page建议YASM有一个WRT
指令,可以控制使用.got
,.plt
等。
根据this page上的S9.2.5,看起来你可以说CALL puts WRT ..plt
(假设Yasm使用相同的语法,因为这是一个NASM参考)
您的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
(桌面上的默认设置,但不是-fpie
上的默认设置)进行编译时会发出https://godbolt.org/,而只是在用call puts
进行编译时使用-fno-pie
。
有关PLT的更多信息,请参阅What does @plt mean here?。
顺便说一句,一个更准确/特定的原型会让gcc在调用foo
之前避免归零EAX:
C中的extern void foo();
意味着extern void foo(...);
您可以将其声明为extern void foo(void);
,这是()
在C ++中的含义。
asm的改进
您还可以将message
放入section .rodata
(只读数据,作为文本段的一部分链接)。
您不需要堆栈帧,只需要在调用之前将堆栈对齐16。假的push rax
会这样做。或者我们可以通过跳转到它而不是调用它来尾随调用puts
,具有与进入此函数时相同的堆栈位置。无论有没有PIE,这都有效。
call puts wrt ..plt
明确要求通过PLTcall [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
并让链接器对其进行排序,除非您需要no-plt样式的动态链接。但是如果你确实选择静态链接libc,我认为这是你获得直接jmp到libc函数的唯一方法。