无法通过汇编(yasm)代码在64位Linux上调用C标准库函数

时间:2018-09-01 08:22:41

标签: c linux assembly x86-64 yasm

我有一个函数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生成类似代码?

2 个答案:

答案 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清零:

C中的

extern void foo();表示extern void foo(...);
您可以将其声明为extern void foo(void);,这就是()在C ++中的含义。


asm改进

您还可以将message放在section .rodata(只读数据,链接为文本段的一部分)中。

您不需要堆栈框架,只需要在调用之前将堆栈按16对齐即可。虚拟push rax可以做到。或者我们可以通过跳转而不是调用它来尾随puts的调用,其栈位置与该函数的入口相同。无论有无PIE,此功能都可以使用。

但是,如果要制作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参考)