【问题标题】:VA (Virtual Address) & RVA (Relative Virtual Address)VA(虚拟地址)和 RVA(相对虚拟地址)
【发布时间】:2011-01-11 08:48:27
【问题描述】:

作为链接器输入的文件称为对象文件。 链接器生成一个图像文件,然后加载器将其用作输入。

来自“Microsoft Portable Executable and Common Object File Format Specification”的简介

RVA(相对虚拟地址)。在图像文件中,项目的地址 在它被加载到内存后,与 图像文件的基地址 从中减去。项目的 RVA 几乎总是与其不同 在磁盘上的文件中的位置(文件 指针)。

在目标文件中,RVA 小于 有意义,因为内存位置 未分配。在这种情况下,RVA 将是一个部分内的地址 (本表稍后描述), 稍后应用重定位 在链接期间。为简单起见,一个 编译器应该只设置第一个 RVA 在每个部分归零。

VA(虚拟地址)。与 RVA 相同,不同之处在于 不减去图像文件。这 地址被称为“VA”,因为 Windows 创建一个独特的 VA 空间 对于每个进程,独立于 物理内存。对于几乎所有 目的,应考虑 VA 只是一个地址。 VA 不像 可预测为 RVA,因为 loader 可能不会在其位置加载图像 首选位置。

即使读了这篇文章,我仍然不明白。我有很多问题。任何人都可以用实际的方式解释它。请按照说明遵守Object FileImage File 的术语。

我所知道的地址是这样的

  • 在目标文件和图像文件中,我们都不知道确切的内存位置,所以,
  • 汇编程序在生成目标文件时计算与.data.text 部分相关的地址(用于函数名称)。
  • 将多个目标文件作为输入的链接器会生成一个图像文件。在生成时,它首先合并每个目标文件的所有部分,并在合并时重新计算相对于每个部分的地址偏移量。而且,没有什么比全局偏移量更好的了。

如果我知道的有问题,请纠正我。

编辑:

阅读弗朗西斯的回答后,我清楚什么是物理地址、VA 和 RVA 以及它们之间的关系。

所有变量和方法的 RVA 必须在重定位期间由链接器计算。那么,(方法/变量的 RVA 值)==(它与文件开头的偏移量)?一定是真的。但令人惊讶的是,它不是。为什么会这样?

我在c:\WINDOWS\system32\kernel32.dll 上使用PEView 进行了检查,发现:

  1. RVA 和 FileOffset 直到 Sections 的开头都是相同的。(.text 是此 dll 中的第一部分)。
  2. .text 的开头到.data,.rsrc 直到最后一节的最后一个字节 (.reloc) RVA 和 FileOffset 不同。 & 并且第一部分的第一个字节的 RVA 是“总是”显示为 0x1000
  3. 有趣的是每个部分的字节在FileOffset中是连续的。我的意思是另一个部分从一个部分的最后一个字节的下一个字节开始。但是,如果我在 RVA 中看到同样的情况,那么这就是一节最后一个字节的 RVA 和下一节的第一个字节之间的巨大差距。

我的猜测:

  1. 所有,数据的字节数 在第一个之前(.text 这里) 部分“未”实际加载 进入过程的 VA 空间,这些 字节的数据只是用来 找到并描述这些部分。 它们可以称为“元节” 数据”。

    因为它们没有加载到 VA 过程空间。的使用 术语 RVA 也没有意义 这是 RVA == FileOffset 这些字节的原因。

  2. 因为,

    • RVA 术语仅对实际加载的字节有效 进入 VA 空间。
    • .text.data.rsrc.reloc的字节就是这样的字节。
    • 而不是从 RVA 0x00000 启动 PEView 软件正在启动 来自0x1000
  3. 我不明白为什么要进行第三次观察。我无法解释。

【问题讨论】:

    标签: assembly linker loader portable-executable


    【解决方案1】:

    大多数 Windows 进程 (*.exe) 加载在(用户模式)内存地址 0x00400000 中,这就是我们所说的“虚拟地址”(VA)——因为它们只对每个进程可见,并且会被转换为不同的操作系统的物理地址(内核/驱动层可见)。

    例如,一个可能的物理内存地址(CPU可见):

    0x00300000 on physical memory has process A's main
    0x00500000 on physical memory has process B's main
    

    并且操作系统可能有一个映射表:

    process A's 0x00400000 (VA) = physical address 0x00300000
    process B's 0x00400000 (VA) = physical address 0x00500000
    

    那么当你尝试在进程A中读取0x004000000时,你会得到位于物理内存0x00300000的内容。

    关于 RVA,它只是为了方便搬迁而设计的。当加载可重定位模块(例如,DLL)时,系统将尝试将其滑过进程内存空间。所以在文件布局中它会放置一个“相对”地址来帮助计算。

    例如,一个 DLL C 可能有这个地址:

     RVA 0x00001000 DLL C's main entry
    

    当被加载到基地址0x10000000的进程A时,C的主入口变成了

     VA = 0x10000000 + 0x00001000 = 0x10001000
     (if process A's VA 0x10000000 mapped to physical address was 0x30000000, then 
      C's main entry will be 0x30001000 for physical address).
    

    当被加载到基地址0x32000000的进程B时,C的主入口变成了

     VA = 0x32000000 + 0x00001000 = 0x32001000
     (if process B's VA 0x32000000 mapped to physical address was 0x50000000, then 
      C's main entry will be 0x50001000 for physical address).
    

    通常图像文件中的 RVA 在加载到内存时是相对于进程基地址的,但有些 RVA 可能是相对于图像或目标文件中的“节”起始地址(您必须检查 PE 格式规范以了解详细信息)。无论如何,RVA 都是相对于“一些”基础 VA。

    总结一下,

    1. 物理内存地址是 CPU 看到的
    2. 虚拟地址 (VA) 与每个进程的物理地址相关(由操作系统管理)
    3. RVA 相对于 VA(文件基或节基),每个文件(由链接器和加载器管理)

    (编辑)关于爪子的新问题:

    方法/变量的 RVA 值并不总是它与文件开头的偏移量。它们通常与某些 VA 相关,这可能是默认加载基地址或节基 VA - 这就是为什么我说您必须检查 PE format spec 以了解详细信息。

    您的工具 PEView 正在尝试显示每个字节的 RVA 以加载基址。由于截面从不同的基数开始,交叉截面时RVA可能会有所不同。

    关于你的猜测,它们非常接近正确答案:

    1. 通常我们不会在节之前讨论“RVA”,但 PE 头仍然会被加载,直到节头结束。不会加载节标题和节正文(如果有)之间的间隙。您可以通过调试器进行检查。此外,当部分之间存在间隙时,它们可能不会加载。

    2. 正如我所说,RVA 只是“相对于某个 VA”,无论它是什么 VA(虽然在谈到 PE 时,VA 通常指的是加载基址)。当您阅读 PE 格式规范时,您可能会发现一些“RVA”,它与某些特殊地址(如资源起始地址)相关。从 0x1000 开始的 PEView 列表 RVA 是因为该部分从 0x1000 开始。为什么是 0x1000?因为链接器为PE头留下了0x1000字节,所以RVA从0x1000开始。

    3. 您错过的是 PE 加载阶段的“部分”概念。 PE 可能包含几个“段”,每个段映射到一个新的起始 VA 地址。例如,这是从 win7 kernel32.dll 转储的:

      #  Name   VirtSize RVA      PhysSize Offset
      1 .text   000C44C1 00001000 000C4600 00000800
      2 .data   00000FEC 000C6000 00000E00 000C4E00
      3 .rsrc   00000520 000C7000 00000600 000C5C00
      4 .reloc  0000B098 000C8000 0000B200 000C6200
      

      有一个不可见的“0 header RVA=0000, SIZE=1000”,它强制 .text 从 RVA 1000 开始。这些部分在加载到内存(即 VA)时应该是连续的,因此它们的 RVA 是连续的。但是,由于内存是按页面分配的,因此它将是页面大小的倍数(4096=0x1000 字节)。这就是为什么 #2 部分从 1000 + C5000 = C6000 开始(C5000 来自 C44C1)。

      为了提供内存映射,这些部分仍必须按一定大小对齐(文件对齐大小 - 由链接器决定。在我上面的示例中,它是 0x200=512 字节),它控制着 PhysSize 字段。偏移量表示“物理PE文件开始的偏移量”。

      所以 headers 占用文件的 0x800 字节(映射到内存时为 0x1000),这是第 #1 节的偏移量。然后通过对齐其数据(c44c1 字节),我们得到 physsize C4600。 C4600+800 = C4E00,正好是第二节的偏移量。

      好的,这与整个 PE 加载内容有关,因此可能有点难以理解...

    (编辑)让我再做一个新的简单总结。

    1. DLL/EXE(PE 格式)文件中的“RVA”通常与“在内存中加载基地址”相关(但并非总是如此,您必须阅读规范)
    2. PE 格式包含一个“节”映射结构,用于将物理文件内容映射到内存中。所以 RVA 并不是真正相对于文件偏移量。
    3. 要计算某个字节的 RVA,您必须找到它在节中的偏移量并添加节基数。

    【讨论】:

    • 感谢您如此清晰的解释。只是一个建议,您能否在 RVA 示例中将 Process A 的名称更改为 Process X。因为,DLL A 和进程 A 都可能导致混淆。 :)
    • 我已将 DLL A 更改为 DLL C。还优化了部分文本以防止混淆。
    • 我已经扩展了我的查询。你能看一下吗?
    • 顺便说一句,你是如何生成Section Table/Section Headers的?我正在尝试使用 dumpbin 来做到这一点,但我无法生成类似的。
    • RVA 重定位是如何完成的?链接器/加载器是否在物理上遍历文件并重写所有jmpjne 等指令 (我很确定这是不可能的,因为 x86 使用多字节指令,这不能可靠100% 的时间被拆卸)?还是.dll中的所有地址都存储在数组中?
    【解决方案2】:

    相对虚拟地址是相对于文件加载地址的偏移量。获得这个想法的最简单方法可能是举个例子。假设您有一个在地址 1000h 处加载的文件(例如 DLL)。在该文件中,您在 RVA 200h 处有一个变量。在这种情况下,该变量的 VA(在 DLL 映射到内存之后)是 1200h(即 DLL 的 1000h 基地址加上变量的 200h RVA(偏移量)。

    【讨论】:

    • >>you have a variable at RVA 200h. 这与什么有关? dll 的开头(dll 的第一个字节)还是相对于它所在的部分? (.data) 在这种情况下。
    • @claws:大多数 RVA 是相对于文件开头给出的,但偶尔(尤其是在查看目标文件而不是可执行文件时)您会看到基于该部分的 RVA。
    猜你喜欢
    • 2016-10-05
    • 2011-08-20
    • 1970-01-01
    • 2018-07-09
    • 2012-11-06
    • 2011-12-12
    • 2015-05-04
    • 2013-05-05
    • 1970-01-01
    相关资源
    最近更新 更多