Can't call C standard library function on 64-bit Linux from assembly (yasm) code

szx*_*szx 7 c linux assembly x86-64 yasm

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
Run Code Online (Sandbox Code Playgroud)

It is called by a C program compiled with GCC:

extern void foo();

int main() {
    foo();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

After disassembling with objdump I see that the call is made with the wrong address:

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
Run Code Online (Sandbox Code Playgroud)

(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 <puts@plt>
Run Code Online (Sandbox Code Playgroud)

i.e. it references puts from the PLT.

Is it possible to tell yasm to generate similar code?

Pet*_*des 8

您的gcc默认情况下正在构建PIE可执行文件(x86-64 Linux中不再允许使用32位绝对地址?)。

我不确定为什么,但是这样做时链接程序不会自动解析call putscall 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,此功能均可使用。只需更换calljmp,只要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文档..got9.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
Run Code Online (Sandbox Code Playgroud)

在位置相关的可执行文件中,可以使用mov edi, messageRIP相对的LEA代替。它的代码较小,可以在大多数CPU的更多执行端口上运行。

在非PIE可执行文件中,您也可以使用call putsjmp puts让链接器对其进行排序,除非您想要更有效的no-plt样式动态链接。但是,如果您选择静态链接libc,我认为这是将jmp直接连接到libc函数的唯一方法。

(我认为静态链接非PIE的可能性就是为什么 ld愿意为非PIE(而不是PIE或共享库)自动生成PLT存根的原因。它要求您说出链接ELF共享对象时的含义。)

如果您确实使用call puts了PIE(call rel32),则只有将与位置无关的实现静态链接puts到您的PIE中才能起作用,因此整个事情就是一个可执行文件,它将在运行时加载到随机地址(通常是动态-linker机制),但根本不依赖libc.so.6


loc*_*g8b 6

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具有相同的语法)。