4

我正在关注 Jack Crenshaw 的编译器教程(如果你看一下我的个人资料,这就是我所有的问题都是关于大声笑的),它刚刚到了引入变量的地步。他评论说 68k 要求一切都“与位置无关”,这意味着它是“与 PC 相关的”。我知道那台 PC 是程序计数器,而在 x86 上它是 EIP。但他使用的语法类似于MOVE X(PC),D0其中 X 是变量名。我已经提前阅读了一些内容,但后来没有说明在 .data 中声明变量。这是如何运作的?要在 x86 中进行这项工作,我会用 in 替换 X(PC)MOV EAX, X(PC)什么?

老实说,我什至不确定这是否应该输出工作代码,但到目前为止它已经完成了,并且我已经向我的编译器添加了代码,添加了适当的头文件等和一个批处理文件来组装、链接和运行结果。

4

2 回答 2

8

下面简要概述了静态分配的全局变量(这是这个问题的意义所在)的真正含义以及如何处理它们。

无论如何,什么是变量

对于机器来说,没有变量这样的东西。它从来没有听说过它们,它从不关心它们,它只是对它们没有概念。它们只是为 RAM 中的特定位置分配一致含义的约定(在虚拟内存的情况下,是地址空间中的位置)。

您实际放置变量的位置取决于您 - 但在合理范围内。如果您要写入它(并且您可能是),它最好位于可写位置,这意味着:该变量的地址应位于已分配和可写的内存区域内。.data 部分只是另一个约定。你不必这样称呼它,你甚至不需要一个单独的部分(你可以让你的 .text 部分可写并在那里分配你的全局变量,如果你真的想要的话),你甚至可以VirtualAllocEx使用像(或等效的)这样的操作系统函数) 在固定位置分配内存并使用它(但不要那样做)。由你决定。但是 .data 部分是放置它们的方便位置。

“分配”变量只是选择一个地址以使变量不与任何其他变量重叠的问题。这并不难,只需按顺序排列它们:var_ptr在要放置它们的任何位置的开头开始一个指针(因此 .data 部分的 VA ,如果您使用链接器,则为 0 ),然后对于每个变量v

  • 的位置lvalign(var_ptr, round_up_to_power_of_2(sizeof(v)))
  • 设置var_ptrl + 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 部分的缩写),但与往常一样,名称无关紧要。

更多的

大多数编译器教科书以不同的数量处理这个主题,尽管通常不是很详细,因为当你深入了解它时:静态变量并不难。当然没有比较汇编的大多数其他方面。此外,某些方面是非常特定于平台的,例如各个部分的详细信息以及事情实际上如何最终成为可执行文件。

一些来源/有用的东西(我在编译器工作时发现所有这些都很有用):

于 2013-08-26T20:00:57.023 回答
4

许多处理器支持 PC 相对或绝对寻址。

然而,在 X86 机器上存在以下限制:

  • 跳转和调用始终是 PC 相关的(除非基于寄存器)
  • 其他地址始终是绝对的(除非基于寄存器)

可以进行 PC 相对寻址的 C 编译器将通过以下方式实现这一点:

  CALL x
x:
  ; Now address "x" is on the stack
  POP EDI
  ; Now EDI contains address of "x"
  ; Now we can do (pseudo-)PC-Relative addressing:
  MOV EAX,[EDI+1234]

如果在编译/链接期间不知道内存中代码的地址(例如,对于 Linux 下的动态库 (DLL)),则使用此选项,因此不知道变量的地址(此处位于地址“x+1234”)已知,然而。

于 2013-08-26T15:54:02.463 回答