6

我正在实现一个简单的 VM,目前我正在使用运行时算法来计算单个程序对象地址作为基指针的偏移量。

我今天就这个主题问了几个问题,但我似乎无处可去。

我从问题一中学到了一些东西—— 对象和结构成员访问以及地址偏移量计算——我了解到现代处理器具有虚拟寻址功能,允许计算内存偏移量而无需任何额外的算术周期。

从问题二开始 -在 C/C++ 编译期间地址偏移是否已解决?- 我了解到手动进行偏移时无法保证会发生这种情况。

现在应该很清楚,我想要实现的是利用硬件的虚拟内存寻址特性并从运行时卸载这些特性。

我正在使用 GCC,至于平台 - 我正在 Windows 中的 x86 上进行开发,但由于它是一个 VM,我希望它能够在 GCC 支持的所有平台上有效地运行。

因此,欢迎提供有关该主题的任何信息,并将不胜感激。

提前致谢!

编辑:关于我的程序代码生成的一些概述 - 在设计阶段,程序被构建为树层次结构,然后递归序列化为一个连续的内存块,以及索引对象并计算它们从程序内存块开始的偏移量.

编辑2:这是VM的一些伪代码:

switch *instruction
   case 1: call_fn1(*(instruction+1)); instruction += (1+sizeof(parameter1)); break;
   case 2: call_fn2(*(instruction+1), *(instruction+1+sizeof(parameter1));
           instruction += (1+sizeof(parameter1)+sizeof(parameter2); break;
   case 3: instruction += *(instruction+1); break;  

情况 1 是一个接受一个参数的函数,该参数在指令之后立即找到,因此它作为指令的 1 个字节的偏移量传递。指令指针递增 1 + 第一个参数的大小以查找下一条指令。

情况 2 是一个接受两个参数的函数,和之前一样,第一个参数作为 1 字节偏移量传递,第二个参数作为 1 字节偏移量加上第一个参数的大小传递。指令指针然后增加指令的大小加上两个参数的大小。

情况 3 是 goto 语句,指令指针增加一个偏移量,该偏移量紧跟 goto 指令。

编辑3:据我了解,操作系统将为每个进程提供自己专用的虚拟内存寻址空间。如果是这样,这是否意味着第一个地址总是......以及零,所以从内存块的第一个字节的偏移实际上就是这个元素的地址?如果内存地址专用于每个进程,并且我知道我的程序内存块的偏移量以及每个程序对象相对于内存块第一个字节的偏移量,那么对象地址是否在编译时解析?

问题是这些偏移量在 C 代码编译期间不可用,它们在“编译”阶段和转换为字节码期间变得已知。这是否意味着没有办法为“免费”进行对象内存地址计算?

例如,在 Java 中这是如何完成的,只有虚拟机被编译为机器代码,这是否意味着对象地址的计算会因为运行时算法而降低性能?

4

4 回答 4

2

这里试图阐明链接的问题和答案如何适用于这种情况。

第一个问题的答案混合了两个不同的东西,第一个是 X86 指令中的寻址模式,第二个是虚拟到物理地址的映射。第一个是由编译器完成的,第二个是(通常)由操作系统设置的。在您的情况下,您应该只担心第一个。

X86 汇编中的指令在如何访问内存地址方面具有很大的灵活性。读取或写入内存的指令具有根据以下公式计算的地址:

segment + base + index * size + offset

地址的段部分几乎总是默认DS段,通常可以忽略。该base部分由通用寄存器之一或堆栈指针给出。该index部分由通用寄存器之一给出,大小为 1、2、4 或 8。最后,偏移量是嵌入在指令中的常量值。这些组件中的每一个都是可选的,但显然至少必须给出一个。

这种寻址能力通常是指在没有明确的算术指令的情况下计算地址。其中一位评论者提到了一条特殊指令:LEA它执行地址计算,但不是读取或写入内存,而是将计算的地址存储在寄存器中。

对于您在问题中包含的代码,编译器很可能会使用这些寻址模式来避免显式算术指令。

例如,instruction变量的当前值可以保存在ESI寄存器中。此外,每个sizeof(parameter1)sizeof(parameter2)都是编译时常量。在标准的 X86 调用约定中,函数参数以相反的顺序推送(因此第一个参数位于堆栈的顶部),因此汇编代码可能看起来像

case1: 
  PUSH [ESI+1]
  CALL fn1
  ADD ESP,4 ; drop arguments from stack
  ADD ESI,5
  JMP end_switch
case2: 
  PUSH [ESI+5]
  PUSH [ESI+1]
  CALL fn2
  ADD ESP,8 ; drop arguments from stack
  ADD ESI,9
  JMP end_swtich
case3:
  MOV ESI,[ESI+1]
  JMP end_switch
end_switch:

这是假设两个参数的大小都是 4 个字节。当然,实际代码取决于编译器,只要您要求进行某种级别的优化,编译器就会输出相当高效的代码是合理的。

于 2012-07-15T20:49:23.853 回答
1

您在虚拟机中有一个数据项X,位于相对地址A,并且有一条指令说(例如)push X,对吗?并且您希望能够执行该指令,而无需添加AVM 数据区的基地址。

我编写了一个虚拟机,通过将虚拟机的数据区域映射到一个固定的虚拟地址来解决这个问题。编译器知道这个虚拟地址,因此可以A在编译时进行调整。这个解决方案对你有用吗?你能自己改变编译器吗?

我的虚拟机在智能卡上运行,并且我可以完全控制操作系统,因此它与您的环境非常不同。但是 Windows 确实有一些用于在固定地址分配内存的工具——例如VirtualAlloc函数。你可能想试试这个。如果您尝试一下,您可能会发现 Windows 分配的区域与您的固定地址数据区域冲突,因此您可能必须在分配 VM 的数据区域后手动加载您使用的任何 DLL。

但是可能会有无法预料的问题需要克服,而且可能不值得麻烦。

于 2012-07-15T19:04:51.653 回答
0

使用虚拟地址转换、页表或 TLB 只能在 OS 内核级别完成,并且在平台和处理器系列之间是不可移植的。此外,大多数 CPU ISA 上的硬件地址转换通常仅在某些页面大小的级别上得到支持。

于 2012-07-15T18:33:05.223 回答
0

根据我得到的许多回答来回答我自己的问题。

事实证明,在我的情况下,我想要实现的目标实际上是不可能的,只有在满足特定要求并且需要编译为机器特定指令时,才能免费获得内存地址计算。

我正在为教育目的开发一个视觉元素、乐高风格的拖放编程环境,它依赖于一个简单的 VM 来执行程序代码。我希望最大限度地提高性能,但在我的场景中这是不可能的。这没什么大不了的,因为程序元素也可以生成它们的 C 代码等价物,然后可以按照常规方式对其进行编译以最大限度地提高性能。

感谢所有回复并澄清了我不太清楚的问题的人!

于 2012-07-15T20:07:12.517 回答