我正在实现一个简单的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中是如何做到这一点的,其中只有虚拟机被编译为机器代码,这是否意味着对象地址的计算会因为运行时算法而受到性能损失?
这里试图阐明链接的问题和答案如何应用于这种情况。
第一个问题的答案混合了两个不同的东西,第一个是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个字节。当然,实际的代码取决于编译器,只要你要求进行某种级别的优化,编译器就会输出相当高效的代码,这是合理的。
您在虚拟机中有一个数据项X
,位于相对地址A
,并且有一条指令说(例如)push X
,对吗?您希望能够执行此指令,而不必将A
添加到VM数据区域的基地址。
我写了一个虚拟机,通过将虚拟机的数据区域映射到一个固定的虚拟地址来解决这个问题。编译器知道这个虚拟地址,因此可以在编译时调整A
。这个解决方案对你有用吗?你能自己更改编译器吗?
我的虚拟机运行在智能卡上,我可以完全控制操作系统,所以它与您的环境非常不同。但Windows确实有一些在固定地址分配内存的功能,例如VirtualAlloc函数。你可能想试试这个。如果你尝试了一下,你可能会发现Windows正在分配与你的固定地址数据区域冲突的区域,所以你可能必须手动加载你使用的任何DLL,在分配了VM的数据区域之后。
但可能会有一些无法预见的问题需要克服,这可能不值得麻烦。
使用虚拟地址转换、页表或TLB只能在操作系统内核级别完成,并且在平台和处理器系列之间不可移植。此外,大多数CPU ISAs上的硬件地址转换通常仅在特定页面大小的级别上受支持。
根据我得到的许多回复回答我自己的问题。
事实证明,在我的情况下,我想要实现的目标是不可能的,只有当满足特定要求并需要编译到机器特定指令时,才能免费获得内存地址计算。
我正在开发一个视觉元素,lego风格的拖放编程环境,用于教育目的,它依赖于一个简单的VM来执行程序代码。我希望能最大限度地提高性能,但在我的情况下这是不可能的。这没什么大不了的,因为程序元素也可以生成等效的C代码,然后可以按常规进行编译,以最大限度地提高性能。
感谢所有回应并澄清我不太清楚的事情的人!