x86-64 上的 C++:结构/类何时在寄存器中传递和返回?

问题描述 投票:0回答:2

假设 Linux 上的 x86-64 ABI,在 C++ 中的什么条件下,结构会传递给寄存器中的函数,还是传递给堆栈上的函数?在什么条件下它们会返回到寄存器中?课程的答案会改变吗?

如果有助于简化答案,您可以假设单个参数/返回值并且没有浮点值。

c++ assembly x86-64 calling-convention abi
2个回答
27
投票

ABI 规范定义于此处
这里有新版本。

我假设读者已经习惯了文档的术语并且他们可以对原始类型进行分类。


如果对象大小大于两个八字节,则在内存中传递:

struct foo
{
    unsigned long long a;
    unsigned long long b;
    unsigned long long c;               // Commenting this gives mov rax, rdi
};

unsigned long long foo(struct foo f)
{ 
  return f.a;                           // mov     rax, QWORD PTR [rsp+8]
} 

如果是非POD,则在内存中传递:

struct foo
{
    unsigned long long a;
    foo(const struct foo& rhs){}            // Commenting this gives mov rax, rdi
};

unsigned long long foo(struct foo f)
{
  return f.a;                               // mov     rax, QWORD PTR [rdi]
}

复制省略在这里发挥作用

如果包含未对齐的字段,则它在内存中传递:

struct __attribute__((packed)) foo         // Removing packed gives mov rax, rsi
{
    char b;
    unsigned long long a;
};

unsigned long long foo(struct foo f)
{
  return f.a;                             // mov     rax, QWORD PTR [rsp+9]
}

如果以上都不成立,则考虑对象的字段。
如果其中一个字段本身就是一个结构/类,则递归应用该过程。
目标是对对象中的两个八字节 (8B) 中的每一个进行分类。

考虑每个8B的字段类别。
请注意,由于上述对齐要求,整数个字段始终占据 1 个 8B。

设置 C 为 8B 的类别,D 为考虑类别中的字段的类别。

new_class
伪定义为

cls new_class(cls D, cls C)
{
   if (D == NO_CLASS)
      return C;

   if (D == MEMORY || C == MEMORY)
      return MEMORY;

   if (D == INTEGER || C == INTEGER)
      return INTEGER;

   if (D == X87 || C == X87 || D == X87UP || C == X87UP)
      return MEMORY;

   return SSE;
}

然后 8B 的类别计算如下

C = NO_CLASS;

for (field f : fields)
{
    D = get_field_class(f);        // Note this may recursively call this proc
    C = new_class(D, C);
}

一旦我们有了每个 8B 的类别,比如 C1 和 C2,那么

if (C1 == MEMORY || C2 == MEMORY)
    C1 = C2 = MEMORY;

if (C2 == SSEUP AND C1 != SSE)
   C2 = SSE;

注意:这是我对ABI文档中给出的算法的解释。


示例

struct foo
{
    unsigned long long a;
    long double b;
};

unsigned long long foo(struct foo f)
{
  return f.a;
}

8B 及其领域

第一个8B

a
第二个8B
b

a
是 INTEGER,所以前 8B 是 INTEGER。
b
是 X87 和 X87UP,所以第二个 8B 是 MEMORY。 两个 8B 的最后一堂课都是 MEMORY。


示例

struct foo
{
    double a;
    long long b;
};

long long foo(struct foo f)
{
  return f.b;                     // mov rax, rdi
}

8B 及其领域

第一个8B

a
第二个8B
b

a
是SSE,所以第一个8B是SSE。
b
是 INTEGER,所以第二个 8B 是 INTEGER。

最终班级是计算出来的。


返回值

值会相应地返回到它们的类:

  • 内存
    调用者将一个隐藏的第一个参数传递给函数,以便将结果存储到其中。
    在 C++ 中,这通常涉及复制省略/返回值优化。 该地址必须返回到

    eax
    ,从而“通过引用”将 MEMORY 类返回到隐藏的调用者分配的缓冲区。

    如果类型具有 MEMORY 类,则调用者为返回提供空间 值并在 %rdi 中传递此存储的地址,就好像它是第一个一样 函数的参数。实际上,该地址首先成为“隐藏”地址 争论。 返回时 %rax 将包含由 %rax 传入的地址 %rdi 中的呼叫者。

  • 整数指针
    根据需要寄存器

    rax
    rdx

  • SSESSEUP 根据需要寄存器

    xmm0
    xmm1

  • X87X87UP 寄存器

    st0


POD

技术定义是这里

ABI 的定义如下。

如果 de/构造函数是隐式声明的默认 de/构造函数,并且如果:

   • 它的类没有虚函数,也没有虚基类,并且
   • 其类的所有直接基类都有简单的 de/构造函数,并且
   • 对于其类中属于类类型(或其数组)的所有非静态数据成员,每个此类都有一个简单的 de/构造函数。


注意每个8B都是独立分类的,这样每一个都可以相应通过。
特别是,如果没有剩余的参数寄存器,它们可能最终会进入堆栈。


6
投票

x86-64 ABI 已记录在此处,其中版本为 252(我的回答中的最新 ABI),可在此处下载。

如果我正确阅读了第 21 页及以下内容,它说如果 sizeof(struct) 是 8 字节或更少,那么它将在普通寄存器中传递。 之后规则变得复杂,但我认为如果是 9-16 字节,它可能会在 SSE 寄存器中传递。

对于类,请记住类和结构之间的唯一区别是默认访问。 但是规则明确指出,如果存在非平凡的复制构造函数或非平凡的析构函数,则该结构将作为隐藏引用传递。

© www.soinside.com 2019 - 2024. All rights reserved.