2

我一直在 linux 上编写一些 x64 程序集——确切地说它是不相关的——我遇到了一个奇怪的问题。在我的汇编代码中,我已将其声明printf为外部标签,并使用 x64 Linux C 调用约定来调用它。相关位看起来像这样:

extern printf

segment .rodata
    fmt db "%lld", 0x0a, 0x00

segment .text

    mov     rsi, rax ; i64 i want to print
    mov     rdi, fmt ; pointer to the format string
    call    printf

使用 组装nasm -f elf64 file.asm,我得到一个正确的目标文件。与 GNU ld, 链接ld -o file -lc file.o,我得到了一个可以正确运行并产生预期输出的可执行文件。到目前为止,一切都很好。

当我尝试对模具和 lld 做同样的事情时,奇怪的一点就出现了。对于初学者来说,他们都不知道在哪里可以临时找到 libc。这可以; 我问 GCC 在哪里可以找到 libc(gcc --print-file-name libc.so-或者libc.a,我的系统两者都在同一个目录中),答案是/usr/lib. 因此,我尝试再次将我的目标文件与模具和 lld 链接起来,就像这样.. mold/ld.lld -o file -L/usr/lib -lc file.o..它们都链接没有任何报告的错误。但是当我运行生成的可执行文件时,它们都出现了段错误。我还没有调查 LLVM ld 版本,但我将模具版本放入 gdb 并发现发生段错误是因为libc printf 实现中的代码执行跳转到0x00...00.

我的问题很简单:出了什么问题,我该如何解决?两者都是信誉良好的链接器,所以我确定问题出在我身上,但我不清楚我做错了什么。我试图研究这个问题,但是,在我公认的粗略搜索中,我找不到任何其他人有类似问题的实例——或者至少,他们公开整理过的任何实例。我缺少一些标志吗?是/usr/lib不是要看的地方?任何援助将不胜感激。

4

1 回答 1

4

printf从 ELF 入口点 ( )调用 libc 函数_start而不首先调用 glibc init 函数仅适用于动态链接的可执行文件;动态链接器调用 libc 的 init 钩子函数,因此它可以在执行到达您的_start.

但是,如果您链​​接静态可执行文件,那么在 printf 期望找到已分配/初始化的标准输出缓冲区之类的数据结构之前就不会发生这种情况。

_start这就是为什么通常不推荐并认为从main. 一些libc 实现不需要调用init 函数,例如MUSL 不需要,IIRC。但是 glibc 可以。

如果链接动态可执行文件,则需要指定正确的动态链接器路径,因为默认值在大多数现代系统上没有用。我很惊讶ld -o file -lc file.o在你的系统上工作;在我的 x86-64 Arch GNU/Linux 上,GNU Binutilsld的默认解释器路径/lib/ld64.so.1不存在。

使用readelf -l ./file并查看 INTERP 标头。例如,这是我从构建中得到的,gcc -nostartfiles -no-pie -o foo foo.o让它通过正确的选项来ld制作一个链接-lc但不链接 CRT 启动文件的动态可执行文件:

  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

如果您在尝试运行时得到“没有这样./file的文件或目录”,这通常是您手动使用链接器时的问题。 strace ./file将显示 execve 系统调用本身返回,-ENOENT但可以使用相同的路径字符串读取它。ls ./filereadelf -a ./file

ELF 解释器的工作方式类似于#!/bin/sh可执行文本文件顶部的行:内核解析该行并运行/bin/sh ./file. 但是对于 ELF 二进制文件,内核还将可执行文件映射到内存中,因此 ELF 解释器不必使用来自用户空间的系统调用来执行此操作。


可能的 ABI 违规会printf在打印前造成段错误

x86-64 System V ABI(和 Windows x64,顺便说一句)需要在函数RSP % 16 == 0 之前call,从而保证RSP % 16 == 8在进入函数时(在调用推送返回地址之后)。

这让函数可以movaps根据需要更有效地在堆栈上复制本地变量。(为什么 x86-64 / AMD64 System V ABI 要求 16 字节堆栈对齐?

在 ELF 入口点,RSP % 16 == 0 由 x86-64 SysV ABI 保证;这不是一个功能。(RSP 指向argc,而不是返回地址)。 因此,如果这是您的整个实际代码,则 RSP 将正确对齐。


在调用像 printf 这样的可变参数函数时,还要求 AL >= XMM args 的数量,但不超过 8。

用于制作可变参数函数的真正旧 GCC 执行计算跳转以跳过将 XMM regs 转储到 VA_ARG 代码可以引用它们的数组的 movaps 存储的确切数量,但现代 GCC 仅使用test al,al/jz跳过所有 8 个或不跳过。那时严格遵循 ABI 的这一部分很重要,但这些天你可能会马虎。

RAX 将在入口处保留垃圾_start,因为动态链接器在到达之前在您的进程中运行。除非这是一个静态可执行文件,在这种情况下 ABI 不保证任何东西,但实际上 Linux 会将寄存器归零以避免泄漏内核信息。

因此,如果 AL 超出 0..8 范围,则 glibc 的现代构建将恰好可以工作,只要您重新打印任何 FP args 时它不为零。当然,最好传递 XMM regs 中的 FP args 的实际数量并遵循 ABI,例如xor eax,eaxormov eax,3或其他。

在实践中,最近构建的 glibc 确实使用movaps了 printf 中的堆栈,而不是用于转储 XMM 寄存器,所以现在你也无法避免违反 ABI 的那部分,即使 AL=0 用于打印非 FP 内容. (类似地,scanf 编译为恰好需要正确堆栈对齐的代码:glibc scanf 从不对齐 RSP 的函数调用时出现分段错误


因此,如果您使用正确的链接器选项,这个确切的代码可能会在动态可执行文件中工作,只会在没有进行 _exit 系统调用的情况下从结尾处崩溃。 我测试了它,这就是发生的事情。

(当然,将输出重定向到文件会使其为空,因为在您发生段错误之前不会刷新完整缓冲的标准输出,因为您没有call exit。是的,您应该call exit,而不是进行 raw eax=231/ syscallexit_group 系统调用将退出而不调用 libc ate​​xit 函数。)

但显然这不是您的完整代码,所以也许您在通话前搞砸了 RSP 对齐?但可能不是,因为您说当您将代码链接到带有ld. 否则您的系统很旧,因此您的 glibc 的 printf 恰好不需要 RSP 对齐。

于 2022-01-06T10:40:07.000 回答