如果位置计数器在链接描述文件中被初始化为太小或太大,则静态可执行段错误

问题描述

我正在尝试为这个程序生成一个静态可执行文件(使用 musl):

main.S:

.section .text
    .global main

main:
    mov $msg,%rdi
    mov $0,%rax
    call printf

    mov %rax,%rdi
    mov $60,%rax
    syscall

msg:
    .ascii "hello world from printf\n\0"

编译命令:

clang -g -c main.S -o main.o

链接命令(musl libc放在musl目录下(1.2.1版本)):

ld main.o musl/crt1.o -o sm -Tstatic.ld -static -lc -lm -Lmusl

链接脚本(static.ld):

ENTRY(_start)
SECTIONS
{
    . = 0x100e8;
}

此配置会生成一个有效的可执行文件,但如果我将位置计数器偏移量更改为 0x100000x20000生成的可执行文件会在启动期间因段错误而崩溃。在调试时,我发现 musl 初始化代码尝试读取程序头(在 aux 向量中接收的位置),并且由于某种原因,由 aux 向量给出的程序头的内存地址在我们的地址空间中未映射。

这种行为的原因是什么?链接描述文件中的计数器偏移到底是什么?除了改变加载地址之外,它如何影响链接输出

注意:当 musl 初始化代码尝试访问程序头时会发生段错误

解决方法

这里有几个问题。

  1. 您的 main.S 有一个堆栈对齐错误:在 x86_64 上,您必须在调用任何其他函数之前将堆栈重新对齐到 16 字节边界(您可以假设入口为 8 字节对齐。
    如果没有这个,我会在 printf 内崩溃,因为 movaps %xmm0,0x40(%rsp)$rsp 未对齐。

  2. 您的链接顺序错误:crt1.o 应该在 before main.o

    之前链接
  3. 当您在开始 SIZEOF_HEADERS == 0xe8 部分之前没有留下 .text 空间时,您将把它留给链接器将程序头放在其他地方,它确实如此。问题是:musl(和许多其他代码)假设文件头和程序头被映射(但 ELF 格式不需要这个)。所以他们崩溃了。

指定起始地址的正确方法:

ENTRY(_start)
SECTIONS
{
    . = 0x10000 + SIZEOF_HEADERS;
}

更新:

为什么顺序很重要?

链接器(通常)将从左到右组装初始值设定项(构造函数)。当您从 main() 调用标准 C 库例程时,您希望标准库在调用 main() 之前 自行初始化。 crt1.o 中的代码负责执行此类初始化。

如果您以错误的顺序链接:crt1.o after main.o,构建可能无法正确进行。您是否能够观察到这一点取决于标准库的实现细节,以及您正在使用它的哪些部分。所以你的二进制文件可能看起来可以正常工作。但是还是以正确的顺序链接对象更好。

我要留 0x10000 空间,标题不够用吗?

您正在干扰内置的默认链接器脚本,而是给它不完整的关于如何在内存中布置程序的规范。当你这样做时,你需要知道链接器将如何反应。不同的链接器会有不同的反应。

binutils ld 的反应是不发出覆盖程序头的 LOAD 段。 ld.lld 的反应不同——它实际上将 .text 移过程序头。

结果二进制文件仍然崩溃,因为二进制布局不是内核期望的,内核提供的辅助向量中的 AT_PHDR 地址是错误的。

看起来内核希望第一个 LOAD 段是包含程序头的段。可以说这是内核中的一个错误——ELF 规范中没有任何要求。但是所有普通二进制文件在第一个 LOAD 段都有程序头,所以你只需要这样做(或者说服内核开发人员添加代码来处理你奇怪的二进制布局) .