问题描述
假设这个简单的代码:
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
,因为它是为空保留的)?
解决方法
main 的起始地址可以是随机的吗?因为它们是虚拟的,我猜它可以是任何价值,因为虚拟内存会处理其余的事情?即我可以从字面上的 0x1 开始(不是 0x0,因为它是为空保留的)?
内存是虚拟的并不意味着所有的虚拟地址空间都可以随意使用。在大多数操作系统上,可执行模块(程序和库)需要使用地址空间的一个子集,否则加载器将拒绝加载它们。这当然是高度依赖于平台的。
所以地址可以是任何你想要的,只要它在平台特定的范围内。我怀疑任何平台都允许 0x1,这不仅是因为某些平台需要将代码与大于字节的内容对齐。
此外,在许多平台上,地址只是提示:如果它们可以按原样使用,加载程序不必重新定位二进制文件中的给定部分。否则,它会将其移动到可用的地址空间块中。这是相当普遍的,例如在 Windows 上,32 位二进制文件(例如 DLL)具有基地址:如果可用,加载程序可以更快地加载二进制文件。因此,在“初始地址”为 0x1 的假设情况下,假设对齐不是问题,那么地址最终将被移动到地址空间中的其他位置。
还值得注意的是,“初始地址”是一个不明确的术语。可执行文件启动时加载的二进制模块由类似于节的内容组成。每个部分都有自己的基地址,可能还有内部(相对)地址或列表中的地址引用。此外,一个或多个可执行部分也将有一个“入口”地址。加载器将使用这些地址来执行初始化代码(例如 Windows 上的 DllMain
概念)——该代码总是快速返回。最终,没有其他任何依赖的部分将有一个适当命名的入口点,并且将成为您编写的“实际”程序 - 只有在程序退出时才会继续运行并返回。此时控制权可能会返回到加载器,加载器会注意到没有其他东西要执行,并且进程将被拆除。所有这些的细节都高度依赖于平台 - 我只是给出了一个高层次的概述,并不是在任何特定平台上都是这样做的。
链接器是如何得出初始地址的? (还是起始地址是随机的?)
链接器不知道自己要做什么。当你链接你的程序时,链接器会得到平台本身附带的几个文件。这些文件是使代码能够启动所需的链接器脚本和各种静态库。链接器脚本为链接器提供了可以分配地址的约束条件。所以这又是高度特定于平台的。链接器可以以完全确定的方式分配地址,即。相同的输入总是产生相同的输出,或者可以告诉它随机分配某些类型的地址(当然以非重叠方式)。这就是所谓的 ASLR(地址空间随机化)。
,不确定 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? 或 https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/