如何解决《Can'tcallCstandardlibraryfunctionon64-bitLinuxfromassembly(yasm)code》经验,为你挑选了2个好方法。
I have a function foo
written in assembly and compiled with yasm and GCC on Linux (Ubuntu) 64-bit. It simply prints a message to stdout using puts()
, here is how it looks:
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
It is called by a C program compiled with GCC:
extern void foo();
int main() {
foo();
return 0;
}
Build commands:
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
Here is the problem:
When running the program it prints an error message and immediately segfaults during the call to puts
:
./foo: Symbol `puts' causes overflow in R_X86_64_PC32 relocation
Segmentation fault
After disassembling with objdump I see that the call is made with the wrong address:
0000000000000660 :
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 <-- here
671: 5d pop %rbp
672: c3 retq
(671 is the address of the next instruction, not address of puts
)
However, if I rewrite the same code in C the call is done differently:
645: e8 c6 fe ff ff callq 510
i.e. it references puts
from the PLT.
Is it possible to tell yasm to generate similar code?
1> Peter Cordes..:
您的gcc默认情况下正在构建PIE可执行文件(x86-64 Linux中不再允许使用32位绝对地址?)。
我不确定为什么,但是这样做时链接程序不会自动解析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进行编译时会发出call puts@plt
(GAS语法)-fpie
(您的桌面上的默认设置,而不是https://godbolt.org/上的默认设置),而只是call puts
在使用时进行编译的原因-fno-pie
。
请参阅@plt在这里是什么意思?有关PLT的更多信息,以及几年前Linux上动态库的抱歉状态。(现代gcc -fno-plt
就像该博客文章中的想法之一。)
顺便说一句,更准确/更具体的原型将使gcc避免在调用前将EAX归零foo
:
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]
-像gcc的-fno-plt
代码生成样式一样,通过GOT条目进行间接调用。(使用相对于RIP的寻址模式来到达GOT,因此使用rel
关键字)。
WRT =关于。NASM手册文档wrt ..plt
,另请参见第7.9.3节:特殊符号和WRT。
通常,您将使用default rel
文件的顶部,以便可以实际使用call [puts wrt ..got]
并且仍然获得相对于RIP的寻址模式。您不能在PIE或PIC代码中使用32位绝对寻址模式。
call [puts wrt ..got]
使用动态链接存储在GOT中的函数指针将其汇编为内存间接调用。(早期绑定,而不是惰性动态链接。)
NASM文档..got
在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 ; remove the padding
ret
在位置相关的可执行文件中,可以使用mov edi, message
RIP相对的LEA代替。它的代码较小,可以在大多数CPU的更多执行端口上运行。
在非PIE可执行文件中,您也可以使用call puts
或jmp puts
让链接器对其进行排序,除非您想要更有效的no-plt样式动态链接。但是,如果您选择静态链接libc,我认为这是将jmp直接连接到libc函数的唯一方法。
(我认为静态链接非PIE的可能性就是为什么 ld
愿意为非PIE(而不是PIE或共享库)自动生成PLT存根的原因。它要求您说出链接ELF共享对象时的含义。)
如果您确实使用call puts
了PIE(call rel32
),则只有将与位置无关的实现静态链接puts
到您的PIE中才能起作用,因此整个事情就是一个可执行文件,它将在运行时加载到随机地址(通常是动态-linker机制),但根本不依赖libc.so.6
2> lockcmpxchg8..:
该0xe8
操作码后面跟着一个符号偏移量被应用到PC(已经由时间推进到下一指令)来计算分支目标。因此objdump
,将分支目标解释为0x671
。
YASM正在渲染零,因为它可能已在该偏移量上放置了重定位,这就是它要求加载程序puts
在加载期间填充正确偏移量的方式。加载程序在计算重定位时遇到溢出,这可能表明它puts
与您的调用之间的偏移量比32位带符号偏移量所表示的偏移量还大。因此,加载程序无法修复此指令,您将当机。
66c: e8 00 00 00 00
显示未填充的地址。如果您在重定位表中查找,您应该在上看到重定位0x66d
。汇编器使用全零的重定位填充地址/偏移量并不少见。
此页面提示YASM有一个WRT
指令,可以控制使用.got
,.plt
等等。
根据NASM文档上的 S9.2.5 ,看起来您可以使用CALL puts WRT ..plt
(假定YASM具有相同的语法)。