链接器如何生成最终的虚拟内存地址?

How does the linker generate final virtual memory addresses?

假设这个简单的代码:

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    [=11=], -4(%rbp)
100003fad: 5d                           popq    %rbp
100003fae: c3                           retq

我知道0x100003fa0(作为例子)是一个虚拟内存地址。 OS 将在我的程序加载时将其映射到物理内存。

2 个问题:

1- main的首地址可以是随机的吗?因为它们是虚拟的,所以我猜它可以 虚拟内存会处理其余的任何值吗?也就是说,我可以从字面上从 0x1 开始(不是 0x0 因为它是为 null 保留的)?

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

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

通常你不需要玩这个,因为你的工具链要么是为主机构建的,要么是在使用正确的目标设置进行交叉编译时构建的。

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

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

Can the initial address of main be random? as they are virtual I'm guessing it can be any value as the virtual memory will take care of the rest? i.e I can start literally from 0x1 (not 0x0 as it's reserved for null)?

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

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

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

中的其他位置。

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

How does the linker come up with the initial address? (again is the starting address random?)

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