【问题标题】:How does the linker generate final virtual memory addresses?链接器如何生成最终的虚拟内存地址?
【发布时间】:2021-01-31 20:11:31
【问题描述】:

假设这个简单的代码:

int main(){return 0;}

使用objdump我们可以看到内存地址:

0000000100003fa0 _main:
100003fa0: 55                           pushq   %rbp
100003fa1: 48 89 e5                     movq    %rsp, %rbp
100003fa4: 31 c0                        xorl    %eax, %eax
100003fa6: c7 45 fc 00 00 00 00         movl    $0, -4(%rbp)
100003fad: 5d                           popq    %rbp
100003fae: c3                           retq

我知道0x100003fa0(例如)是一个虚拟内存地址。 加载我的程序时,操作系统会将其映射到物理内存。

2 个问题:

1- main 的起始地址可以是随机的吗?因为它们是虚拟的,我猜它可能是 任何价值,因为虚拟内存会照顾其余的?即我可以从 0x1 开始(而不是 0x0,因为它保留为 null)?

2- 链接器如何得出初始地址? (又是起始地址是随机的吗?)

【问题讨论】:

  • 即基地址+偏移量,前者通常可以在构建镜像时设置为链接器配置的一部分。有时为了加快消耗数十或数百个可加载模块的项目的加载时间,通常会“重新设置”模块以确保它们每个都加载到不被其他任何人占用的虚拟空间中(因此不需要在加载时重新定位) .随着ASLR 的出现,最前沿的准备几乎毫无价值。你仍然可以强迫它,但你最好有充分的理由这样做。

标签: c memory memory-management linker virtual-memory


【解决方案1】:

main的起始地址可以是随机的吗?因为它们是虚拟的,我猜它可以是任何价值,因为虚拟内存会处理其余的事情?即我可以从 0x1 开始(不是 0x0,因为它是为 null 保留的)?

内存是虚拟的并不意味着所有的虚拟地址空间都是你可以随意使用的。在大多数操作系统上,可执行模块(程序和库)需要使用地址空间的子集,否则加载器将拒绝加载它们。当然,这高度依赖于平台。

所以地址可以是任何你想要的,只要它在特定于平台的范围内。我怀疑任何平台都会允许 0x1,不仅仅是因为某些平台需要将代码对齐到大于一个字节的东西。

此外,在许多平台上,地址只是提示:如果它们可以按原样使用,加载程序就不必重新定位二进制文​​件中的给定部分。否则,它会将其移动到可用的地址空间块中。这是相当普遍的,例如在 Windows 上,32 位二进制文​​件(例如 DLL)具有基地址:如果可用,加载程序可以更快地加载二进制文件。因此,在“初始地址”为 0x1 的假设情况下,假设对齐不是问题,地址最终会被移动到地址空间中的其他位置。

还值得注意的是,“初始地址”是一个不明确的术语。可执行文件启动时加载的二进制模块由类似于部分的内容组成。每个部分都有自己的基地址,可能还有内部(相对)地址或列表中的地址引用。此外,一个 或多个可执行部分也将有一个“入口”地址。加载程序将使用这些地址来执行初始化代码(例如 Windows 上的 DllMain 概念) - 该代码总是快速返回。最终,没有其他依赖的部分之一将有一个适当命名的入口点,并将成为您编写的“实际”程序 - 一个将继续运行并仅在程序退出时返回。此时控制可能会返回到加载程序,加载程序会注意到没有其他内容要执行,并且进程将被拆除。所有这一切的细节都高度依赖于平台 - 我只是提供一个高级概述,并不是在任何特定平台上都以这种方式完成的。

链接器如何得出初始地址? (又是起始地址是随机的吗?)

链接器不知道自己要做什么。当您链接您的程序时,链接器会收到平台本身附带的更多文件。这些文件是使代码能够启动所需的链接器脚本和各种静态库。链接器脚本为链接器提供了它可以分配地址的约束。所以这又是高度特定于平台的。链接器可以以完全确定的方式分配地址,即。相同的输入总是产生相同的输出,或者可以告诉它随机分配某些类型的地址(当然是以非重叠的方式)。这就是所谓的 ASLR(地址空间随机化)。

【讨论】:

  • 确认一下,虚拟内存本质上是在链接时分配给二进制文件的。如果我想在运行时使用超出该范围的任何东西,我会得到一个段错误,对吗?
  • 不,虚拟内存在加载期间分配给二进制文件,假设您使用某些操作系统,例如 Linux、Solaris、Windows、OS X、iOS。如果您愿意,您只会“获得”一个段错误:“段错误”是指某个 CPU 生成的运行时异常未得到处理。大多数“segfaults”由内核处理,负责将二进制文件的页面从文件存储带入 RAM。链接将在每个部分内分配相对地址,并将部分之间的关​​系制成表格,以便加载器在您运行程序时解决所有问题。
【解决方案2】:

不确定 Visual C 但 gcc(或者更确切地说是 ld)使用链接描述文件来确定最终地址。这可以使用 -T 选项指定。 gcc 链接器脚本的完整详细信息可以在以下位置找到:https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts

通常您不需要使用它,因为您的工具链将为主机构建,或者在使用目标的正确设置进行交叉编译时构建。

对于 ASLR 和 .so 文件,您需要使用 -PIC 或 -PIE(与位置无关的代码或与位置无关的可执行文件)进行编译。您编译的代码将仅包含针对内存中某些基地址的偏移量。然后在运行您的应用程序之前,由操作系统加载程序设置基地址。

【讨论】:

  • 可能要复杂得多
  • 几乎所有事情都是如此
【解决方案3】:

这些地址是基地址和偏移量。 ELF 文件包含有关如何在程序加载时计算实际地址的特殊信息。这是一个相当高级的主题,但如何加载和执行 .elf 文件,您可以在此处阅读:How do I load and execute an ELF binary executable manually?https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-07
    • 2021-01-07
    • 2018-07-09
    • 2016-09-26
    • 2013-10-10
    • 2023-03-17
    • 2021-07-25
    • 2019-01-08
    相关资源
    最近更新 更多