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

问题描述 投票:4回答:2

我有一个用汇编编写的函数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生成类似的代码吗?

c linux assembly x86-64 yasm
2个回答
4
投票

已删除注释的已清理版本

在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参考)


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(桌面上的默认设置,但不是-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,这都有效。

但是如果你想制作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并让链接器对其进行排序,除非您需要no-plt样式的动态链接。但是如果你确实选择静态链接libc,我认为这是你获得直接jmp到libc函数的唯一方法。

© www.soinside.com 2019 - 2024. All rights reserved.