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

2024-09-30 14:02:00
admin
原创
88
摘要:问题描述:我有一个用汇编语言编写的函数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 具有相同的语法)。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用