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

2024-09-30 14:02:00
admin
原创
223
摘要:问题描述:我有一个用汇编语言编写的函数foo,在 Linux (Ubuntu) 64 位上用 yasm 和 GCC 编译。它只是使用 将消息打印到 stdout puts(),如下所示:bits 64 extern puts global foo section .data message: db ...

问题描述:

我有一个用汇编语言编写的函数foo,在 Linux (Ubuntu) 64 位上用 yasm 和 GCC 编译。它只是使用 将消息打印到 stdout puts(),如下所示:

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>

即,它引用puts了 PLT。

是否可以告诉 yasm 生成类似的代码?


解决方案 1:

TL:DR: 3 个选项:

  • 构建一个非 PIE 可执行文件(gcc -no-pie -fno-pie call-lib.c libcall.o),以便当您写入时,链接器将为您透明地生成 PLT 条目call puts

  • call puts wrt ..plt 就像gcc -fPIE会做的那样。

  • call [rel puts wrt ..got]就像gcc -fno-plt会做的那样。

后两种方法可以在 PIE 可执行文件或共享库中使用。第三种方法wrt ..got效率略高一些。


您的 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 函数。)

无论如何,这就是为什么 gcccall puts@plt在使用 编译时会发出(GAS 语法) (这是桌面上的默认设置,但不是<https://godbolt.org/>-fpie上的默认设置),但仅在使用 编译时发出。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++ 不允许未指定参数的函数声明。


asm 改进

您还可以输入messagesection .rodata只读数据,链接为文本段的一部分)。

您不需要堆栈框架,只需要在调用之前将堆栈对齐 16。虚拟函数push rax就可以完成此操作。

或者我们可以puts通过跳转到尾部调用而不是调用它来进行尾部调用,堆栈位置与进入此函数时相同。无论有没有 PIE,这都可以工作。只要 RSP 指向您自己的返回地址,只需将其替换call为即可。jmp

如果你想制作 PIE 可执行文件(或共享库),你有两个选择

  • call puts wrt ..plt- 明确通过 PLT 调用。

  • call [rel puts wrt ..got]- 明确地通过 GOT 条目进行间接调用,就像 gcc 的-fno-plt代码生成风格一样。(使用 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&#039;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 &#039;foo() called&#039;, 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                   ; restore the stack, undoing the add
    ret

在位置相关的Linux 可执行文件中,您可以使用mov edi, messageRIP 相对 LEA 来代替。它的代码大小更小,可以在大多数 CPU 上的更多执行端口上运行。(有趣的事实:MacOS 总是将“映像基数”放在低 4GiB 之外,因此这种优化在那里是不可能的。)

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

(我认为非 PIE 静态链接的可能性就是为什么 ld愿意为非 PIE 自动生成 PLT 存根,但不会为 PIE 或共享库生成。它要求您在链接 ELF 共享对象时说明您的意思。)

call puts如果你确实在 PIE 中使用( call rel32),那么只有将位置独立的实现静态链接puts到 PIE 中时,它才能工作,因此整个东西是一个可执行文件,它将在运行时加载到随机地址(通过通常的动态链接器机制),但根本不依赖于libc.so.6


当目标在静态链接时存在时,链接器“放松”调用

GAScall *bar@GOTPCREL(%rip)用途R_X86_64_GOTPCRELX(可放宽)

NASMcall [rel bar wrt ..got]用途R_X86_64_GOTPCREL(不可放宽)

对于手写 asm 来说,这不是什么问题;call bar当你知道符号将出现在你要链接的另一个符号中.o(而不是.so)时,你可以直接使用。但是 C 编译器不知道库函数和你用原型声明的其他用户函数之间的区别(除非你使用gcc -fvisibility=hidden <https://gcc.gnu.org/wiki/Visibility>或属性/指令之类的东西)。

不过,如果您静态链接库,您可能希望编写链接器可以优化的 asm 源,但据我所知,您无法使用 NASM 做到这一点。您可以使用 将符号导出为隐藏符号(在静态链接时可见,但在最终 .so 中动态链接时不可见)global bar:function hidden,但那是在定义函数的源文件中,而不是访问它的文件中。


global bar
bar:
    mov eax,231
    syscall
    call bar wrt ..plt
    call [rel bar wrt ..got]
extern bar

第二个文件,在使用进行汇编nasm -felf64和反汇编之后objdump -drwc -Mintel,可以看到重定位:

0000000000000000 &lt;.text>:
   0:   e8 00 00 00 00          call   0x5      1: R_X86_64_PLT32       bar-0x4
   5:   ff 15 00 00 00 00       call   QWORD PTR [rip+0x0]        # 0xb 7: R_X86_64_GOTPCREL    bar-0x4

ld与(GNU Binutils) 2.35.1链接后-ld bar.o bar2.o -o bar

0000000000401000 &lt;_start>:
  401000:       e8 0b 00 00 00          call   401010 &lt;bar>
  401005:       ff 15 ed 1f 00 00       call   QWORD PTR [rip+0x1fed]        # 402ff8 &lt;.got>
  40100b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

0000000000401010 &lt;bar>:
  401010:       b8 e7 00 00 00          mov    eax,0xe7
  401015:       0f 05                   syscall 

请注意,PLT 形式被放宽为直接的call bar,PLT 被消除。但ff 15call [rel mem] 并没有被放宽为e8 rel32

使用 GAS:

_start:
        call    bar@plt
        call    *bar@GOTPCREL(%rip)

gcc -c foo.s &amp;&amp; disas foo.o

0000000000000000 &lt;_start>:
   0:   e8 00 00 00 00          call   5 &lt;_start+0x5>   1: R_X86_64_PLT32       bar-0x4
   5:   ff 15 00 00 00 00       call   QWORD PTR [rip+0x0]        # b &lt;_start+0xb>      7: R_X86_64_GOTPCRELX   bar-0x4

注意 R_X86_64_GOTPCRELX 末尾的 X

ld bar2.o foo.o -o bar &amp;&amp; disas bar。:

0000000000401000 &lt;bar>:
  401000:       b8 e7 00 00 00          mov    eax,0xe7
  401005:       0f 05                   syscall 

0000000000401007 &lt;_start>:
  401007:       e8 f4 ff ff ff          call   401000 &lt;bar>
  40100c:       67 e8 ee ff ff ff       addr32 call 401000 &lt;bar>

两次调用都放宽为直接e8 call rel32到目标地址。间接调用中的额外字节用67地址大小前缀填充(对没有影响call rel32),将指令填充到相同的长度。(因为重新组装和重新计算函数内的所有相关分支以及对齐等已经太晚了。)

call *puts@GOTPCREL(%rip)如果您静态链接 libc,就会发生这种情况gcc -static

解决方案 2:

操作码0xe8后跟一个有符号偏移量,该偏移量将应用于 PC(此时 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 具有相同的语法)。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用