x86-64 系统 V abi - 参数传递的参数分类

5
在第3.2.3节中,x86_64 System V ABI指定函数调用的哪些参数放入哪些寄存器中以及哪些被推入栈中。我对聚合体分类算法感到困惑,它说(高亮部分为我添加):
聚合类型(结构体和数组)和联合类型的分类方式如下:
1. 如果一个对象的大小大于8个8字节或者它包含不对齐的字段,则它的分类为MEMORY。 2. 如果一个C++对象在调用的目的上是非平凡的,正如C++ ABI规范中所指定的那样,则通过不可见引用传递(该对象在参数列表中被一个具有INTEGER类的指针替换)。 3. 如果聚合量的大小超过单个8字节,则每个聚合量按独立方式进行分类。每个8字节都被初始化为NO_CLASS类。 4. 对象的每个字段递归地进行分类,以便始终考虑两个字段。根据八字节中字段的类别计算出相应的类别:(a) 如果两个类别相等,则这是结果类别,(b) 如果一个类别是NO_CLASS,则结果类别是另一个类别;(c) 如果一个类别是MEMORY,则结果是MEMORY类。(d) 如果一个类别是INTEGER,则结果是INTEGER类。(e) 如果一个类别是X87、X87UP、COMPLEX_X87类,则使用MEMORY作为类。(f) 否则使用SSE类别。 5. 然后进行后期合并清理:(a) 如果其中一个类别是MEMORY,则整个参数都通过内存传递。(b) 如果X87UP没有前置X87,则整个参数都通过内存传递。(c) 如果聚合量的大小超过2个8字节,并且第一个8字节不是SSE,或者任何其他8字节不是SSEUP,则整个参数都通过内存传递。(d) 如果SSEUP没有前置SSE或SSEUP,则它将转换为SSE。
我不理解点(3),(4)和(5)。具体来说,我有以下问题:
Q1.在第(3)点中,“每个分别分类”是指“每个八字节”吗?如果是这样,那么我希望接下来的内容是关于八字节分类的解释。
Q2.在第(4)点中,“对象的每个字段”是指“第(3)点中分离出的每个八字节的每个字段”吗?
Q3.在第(4)点中,“总是考虑两个字段”,它们是指连续的两个字段吗?
Q4.在第(4)点中,“结果类”是指对象的类、八字节的类、第二个被考虑字段的类还是其他什么类?在最后一种情况下,结果类在哪里使用?这是否意味着算法保留第一个字段,并迭代计算下一个字段的类,直到我们得到八字节中所有字段的类?还是它意味着算法一次处理两个字段?
Q5.在第(4)点中,如果只有一个字段,或者有偶数个字段,会怎样?

问题6. 在第5点中,“字段的一个类别”还是“八字节的一个类别”?

如果有人能提供更正式/精确的内容 - 例如,伪代码或流程图 - 那将是理想的。


1
第四点提到了“八字节中的字段”。根据第一点,个别字段的分类仅适用于具有对齐字段的聚合体。正常对齐方式为4字节,因此一个八字节最多可以包含2个字段。我不确定第四点中的“总是两个字段”是否正确。也许这有助于理解规范。 - Bodo
你是对的。我没有考虑到这种情况。 - Bodo
第一点是错误的。应该是“两个八字节”,而不是“八个八字节”。 - EvanL00
@EvanL00:你怎么知道这是“错误”的?引用的文本就是链接文档中的原样;原始文档说:“1.如果一个对象的大小大于八个八字节……”那是一个草案版本,但它也在发布版本中。如果你认为这是原始文件中的错误,因为第4点讨论了两个字段,暗示聚合中只有两个八字节,在第5点中显示涉及超过两个八字节的情况:“如果聚合的大小超过两个八字节……” - Eric Postpischil
@EricPostpischil 这里是更新后的评论。 这个链接 https://c9x.me/compile/doc/abi.html 的第4点提到了“两个八字节”。接下来的s是通过栈传递的,所以它在内存中,对吧?// Type your code here, or load an example. class s { public: int i; int d; int j; int k; int g; int h; }; void g(s pi, int j) { } int main() { s s1; int i = 4; g(s1, i); } - EvanL00
显示剩余2条评论
1个回答

3
请查看gcc实现
针对第1点的澄清(回应评论中称“eight是错别字,应该是2”的说法):
  1. 如果一个对象的大小大于8个八字节,或者它包含不对齐的字段,则其类别为MEMORY。
      /* On x86-64 we pass structures larger than 64 bytes on the stack.  */
      if (bytes > 64)
        return 0;

该函数返回用于参数的寄存器数量,如果返回值为零则意味着应该使用内存。
(稍后,在分析完成后,如果有超过两个 eightbytes,则仅在第一个是SSE且其余为SSEUP的情况下使用寄存器,如5.(c)所指出的那样:
(c) 如果聚合体的大小超过两个8字节,并且第一个8字节不是SSE或任何其他8字节不是SSEUP,则整个参数将通过内存传递。)
Q1. 在第(3)点中,“每个分类别单独处理”,作者是否指“每个eightbyte”?
是的,在代码中,每个eightbyte被称为“word”。
每个eightbyte都被初始化为NO_CLASS类。
  int words = CEIL (bytes + (bit_offset % 64) / 8, UNITS_PER_WORD);
  // ...
      for (i = 0; i < words; i++)
        classes[i] = X86_64_NO_CLASS;

Q2. 在第(4)点中,“每个对象的字段”是指“通过第(3)点分离而成的八字节的每个字段”吗?
不,它们指的是结构体/类、联合体或数组元素的每个字段。代码中处理这些内容的地方有几处,但你会看到像这样的for循环:for
          for (field = TYPE_FIELDS (type); field; field = DECL_CHAIN (field))

这就是为什么它是递归的原因。字段本身可以是聚合类型。整个逻辑从每个字段开始应用,然后通过递归函数:
  • 要么返回0,表示整个内容在内存中传递,
  • 或者返回将使用的寄存器(八字节)数量和每个寄存器的类别(通过嵌套字段的递归将在具有非聚合类型的字段处终止)。
                      num = classify_argument (TYPE_MODE (type), type,
                                               subclasses,
                                               (int_bit_position (field)
                                               + bit_offset) % 512);
                      if (!num)
                        return 0;

Q3. 在第(4)点中,“两个字段”指的是连续的两个字段吗?
我认为这里“字段”不准确。而且不是连续的。它所做的是将迄今确定的每个word类别与相应的字段递归地合并到一起。请参见下文:
                      pos = (int_bit_position (field)
                            + (bit_offset % 64)) / 8 / 8;
                      for (i = 0; i < num && (i + pos) < words; i++)
                        classes[i + pos]
                          = merge_classes (subclasses[i], classes[i + pos]);

pos(该字段所在的八字节)开始,每个类都会与通过递归调用该字段确定的子类合并。


Q4. 在第4点中,“结果类”是指对象的类、八字节的类、第二个考虑的字段的类还是其他什么类?

现在正在描述merge_classes函数,它接受两个类并返回八字节的合并类。我们正在遍历字段,但类是针对八字节的。

在最后一种情况下,结果类在哪里使用?

每个类将确定相应寄存器的类型(GPR/SSE/X87等)。


Q5. 在第4点中,如果只有一个字段呢?或者有偶数个字段呢?

我希望“两个字段”在这个时候已经得到了解答。如果,例如,结构体只有一个字段,则该八字节的类将被初始化为NO_CLASS,然后对于该字段,它将被确定为,例如,INTEGER。然后在合并时,类将变为INTEGER


Q6. 在第5点中,“一个字段的类”是指一个八字节的类还是一个字段的类?

是指一个八字节的类。类始终是指八字节。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接