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 ./file
readelf -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,eax
ormov eax,3
或其他。
在实践中,最近构建的 glibc 确实使用movaps
了 printf 中的堆栈,而不是用于转储 XMM 寄存器,所以现在你也无法避免违反 ABI 的那部分,即使 AL=0 用于打印非 FP 内容. (类似地,scanf 编译为恰好需要正确堆栈对齐的代码:glibc scanf 从不对齐 RSP 的函数调用时出现分段错误)
因此,如果您使用正确的链接器选项,这个确切的代码可能会在动态可执行文件中工作,只会在没有进行 _exit 系统调用的情况下从结尾处崩溃。 我测试了它,这就是发生的事情。
(当然,将输出重定向到文件会使其为空,因为在您发生段错误之前不会刷新完整缓冲的标准输出,因为您没有call exit
。是的,您应该call exit
,而不是进行 raw eax=231
/ syscall
exit_group 系统调用将退出而不调用 libc atexit 函数。)
但显然这不是您的完整代码,所以也许您在通话前搞砸了 RSP 对齐?但可能不是,因为您说当您将代码链接到带有ld
. 否则您的系统很旧,因此您的 glibc 的 printf 恰好不需要 RSP 对齐。