有时候你可能会有这样的疑惑:为什么页面大小是4K,而VirtualAlloc函数分配的内存是以64K(而不是4K)为边界呢?
事情还得从Alpha AXP处理器说起
在Alpha AXP处理器上,没有一条指令对应”加载一个32位的证书”这样的操作,取而代之地,实际上是加载两个16位的整数然后将它们合并成32位的。
所以,如果内存分配粒度小于64K,则一个需要重新定位的DLL将会需要对每一次重新分配的地址做出两次调整:一次是高16位地址,另一次是低16位地址。如果这改变了两半地址之间的进位或借位,情况就会变得更糟。(例如,将4K的数据从0x1234F000移到0x12350000,这将迫使地址的低16位和高15位都发生改变。即使移动的地址量远小于64K,但由于存在进位,这项操作仍然对地址的高16位部分产生影响)
除了这个,还有更糟的
Alpha AXP处理器实际上会合并两个16位的带符号整数到一个32位的整数。例如,为了加载0x1234ABCD,你需要首先使用LDAH指令加载0x1234到目标寄存器的高16位,然后再使用LDA指令加上带符号整数-0x5433(因为,0x5433 = 0x10000 – 0xABCD),才能得到预期的结果0x1234ABCD。
因此,如果重定位导致地址在64K块的”下半部分”和”上半部分”之间移动,则必须进行其他修正,以确保正确调整了地址上半部分的算法。由于编译器喜欢对指令进行重新排序,因此该LDAH指令可能距离很远,因此下半部分的重定位记录将不得不采用某种方式来找到匹配的上半部分。
而且,编译器很聪明,如果需要为同一64K区域中的两个变量计算地址,则它们之间共享LDAH指令。如果可以通过不是64K的倍数的值进行重定位,则编译器将不再能够执行此优化,因为在重定位之后,这两个变量不再属于同一64K内存块中。
强制以64K粒度分配内存可解决所有这些问题。
如果你仔细观察的话,你会发现这也解释了为什么2GB边界附近有64K的”无人区”。考虑一下计算值0x7FFFABCD的方法:由于低16位在64K范围内,该值需要通过减法而不是加法来计算。一种比较想当然的解决方案:
上面的做法行不通
Alpha AXP是64位处理器,0x8000不适合放入到16位带符号整数中,因此必须使用-0x8000(负数)。所以,实际发生的是:
你需要添加第三条指令来清除高32位。一种巧妙的技巧是,将零加起来,然后告诉处理器将结果视为32位整数并将其符号扩展为64位。
如果允许访问2GB边界的64K范围内的地址,则每一次内存地址计算都必须插入上面所提到的第三条ADDL指令,以防万一该地址被重新分配到2GB边界附近的“危险区域”。
要访问地址空间中的最后一个64K区域,会付出一笔非常高的代价:对于所有地址计算,其性能都会受到50%的影响,以避免在实践中永远不会发生这种情况。因此,将这片区域设置为永久禁用,是一种更为明智的选择。