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

问题描述

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

电源:

.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;
}

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

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

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

标签: linkerldelfstatic-linkingmusl

解决方案


这里有几个问题。

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

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

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

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

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

更新:

为什么顺序很重要?

链接器(通常)将从左到右组装初始化器(构造器)。当您从 调用标准 C 库例程时main(),您希望标准库在被调用之前已经初始化。 main()代码中crt1.o负责执行这样的初始化。

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

我要留下 0x10000 空间,对于标题来说还不够吗?

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

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

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

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


推荐阅读