下面简要概述了静态分配的全局变量(这是这个问题的意义所在)的真正含义以及如何处理它们。
无论如何,什么是变量
对于机器来说,没有变量这样的东西。它从来没有听说过它们,它从不关心它们,它只是对它们没有概念。它们只是为 RAM 中的特定位置分配一致含义的约定(在虚拟内存的情况下,是地址空间中的位置)。
您实际放置变量的位置取决于您 - 但在合理范围内。如果您要写入它(并且您可能是),它最好位于可写位置,这意味着:该变量的地址应位于已分配和可写的内存区域内。.data 部分只是另一个约定。你不必这样称呼它,你甚至不需要一个单独的部分(你可以让你的 .text 部分可写并在那里分配你的全局变量,如果你真的想要的话),你甚至可以VirtualAllocEx
使用像(或等效的)这样的操作系统函数) 在固定位置分配内存并使用它(但不要那样做)。由你决定。但是 .data 部分是放置它们的方便位置。
“分配”变量只是选择一个地址以使变量不与任何其他变量重叠的问题。这并不难,只需按顺序排列它们:var_ptr
在要放置它们的任何位置的开头开始一个指针(因此 .data 部分的 VA ,如果您使用链接器,则为 0 ),然后对于每个变量v
:
- 的位置
l
是v
align(var_ptr, round_up_to_power_of_2(sizeof(v)))
- 设置
var_ptr
为l + sizeof(v)
作为一个小的变化,您可以跳过对齐(大多数编译器教科书都这样做,但在现实生活中您应该对齐)。x86 通常可以让您摆脱这种情况。
作为一个更大的变化,您可以尝试“填补路线留下的漏洞”。至少填充大多数孔的最简单方法是仅对变量进行最大排序(如果所有尺寸都是 2 的幂,则填充所有孔)。虽然这可能会节省一些空间(虽然不一定有,因为部分本身是对齐的),但它从来没有节省太多。在通常的对齐规则下,“只是按顺序排列”算法在最坏的情况下会浪费它在孔上使用的空间的近一半。导致这种情况的模式是最小类型和最大类型的交替序列。老实说,这不会真的发生——即使发生了,也不是那么糟糕。
然后,您必须确保 .data 段足够大以容纳所有变量,并且初始内容与初始化变量的内容相匹配。
但你甚至不必做任何这些。您可以在汇编代码中使用变量声明(您知道如何做到这一点),然后汇编器/链接器(它们通常都在其中发挥作用)将为您完成所有这些工作(当然,它也会用变量地址替换变量名)。
如何使用变量
这取决于。如果您使用的是汇编器/链接器,只需参考您为变量提供的标签。当然,标签不必与源代码中的名称匹配,它可以是任何合法的唯一名称(例如,您可以使用声明的 AST 节点 ID,并在其前面加上下划线)。
因此加载变量可能如下所示:
mov eax, dword ptr [variablelabel]
或者,在 x64 上,也许这个
mov eax, dword ptr [rel variablelabel]
这将发出一个相对于 rip 的地址。如果您这样做,您不必关心 RIP 的当前值或变量的分配位置,汇编器/链接器会处理它。在 x64 上,使用这样的 RIP 相对地址很常见,原因如下:
- 它允许 .data 段位于不是第一个 4GB(或 2GB)地址空间的地方,只要它靠近 .text 段
- 它比具有绝对 64 位地址的指令短
- 只有两条指令甚至采用绝对 64 位地址,
mov rax,[imm64]
即mov [imm64],rax
- 您可以免费获得搬迁
如果您没有使用汇编器和/或链接器,那么(至少在某种程度上)您自己的工作就是用您为变量名分配的任何地址替换变量名(如果您使用的是链接器但没有汇编器,您'会制作重定位数据,但您不会自己决定变量的绝对地址)。
当您使用绝对地址时,您可以在发出指令的同时“放入”它们(前提是您已经分配了变量)。当您使用 RIP 相对地址时,您只能在确定代码的位置后将它们放入(因此您会发出偏移量为 0 的代码,做一些簿记,决定代码的位置,然后您返回并用实际偏移量替换 0),这本身就是一个不平凡的问题,除非您使用幼稚的方式并且不关心分支大小优化(在这种情况下,您知道指令的地址你发出它的时间,因此变量相对于 RIP 的偏移量是多少)。相对于 RIP 的偏移量很容易计算,只需减去紧随其后的位置的 RIP当前指令来自变量的 VA(虚拟地址)。
但这还不是全部
您可能希望使某些变量不可写,以至于以“编译无法检测到的有趣方式”写入它们的任何尝试都将失败。这可以通过将它们放入只读部分来完成,通常称为 .rdata(但名称实际上无关紧要,重要的是该部分的“可写”标志是否设置在 PE 标头中)。这并不经常这样做,尽管它有时用于字符串或数组常量(它们不是正确的变量)。
定期做的是将零初始化变量放在它们自己的部分中,该部分在可执行文件中不占用空间,而是简单地清零。将零初始化变量放在可执行文件中可能会节省一些空间。此部分通常称为 .bss(不是Bullsh*t 部分的缩写),但与往常一样,名称无关紧要。
更多的
大多数编译器教科书以不同的数量处理这个主题,尽管通常不是很详细,因为当你深入了解它时:静态变量并不难。当然没有比较汇编的大多数其他方面。此外,某些方面是非常特定于平台的,例如各个部分的详细信息以及事情实际上如何最终成为可执行文件。
一些来源/有用的东西(我在编译器工作时发现所有这些都很有用):