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

jac*_*bsa 8 c++ assembly x86-64 abi

假设Linux上的x86-64 ABI,在C++中的什么条件下结构传递给寄存器中的函数而不是堆栈中的函数?他们在什么条件下返回登记册?课程的答案是否会改变?

如果它有助于简化答案,则可以假设单个参数/返回值而没有浮点值.

Mar*_*oom 11

这里定义 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]
} 
Run Code Online (Sandbox Code Playgroud)

如果它是非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]
}
Run Code Online (Sandbox Code Playgroud)

复制elision在这里工作

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

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]
}
Run Code Online (Sandbox Code Playgroud)

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

考虑每个8B的字段类别.
注意,由于上面的对齐要求,整数个字段总是占据一个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;
}
Run Code Online (Sandbox Code Playgroud)

那么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);
}
Run Code Online (Sandbox Code Playgroud)

一旦我们得到每个8B的类,比如说C1和C2,那么

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

if (C2 == SSEUP AND C1 != SSE)
   C2 = SSE;
Run Code Online (Sandbox Code Playgroud)

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


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

unsigned long long foo(struct foo f)
{
  return f.a;
}
Run Code Online (Sandbox Code Playgroud)

8Bs及其领域

第一个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
}
Run Code Online (Sandbox Code Playgroud)

8Bs及其领域

第一个8B:a 第二个8B:b

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

最后的类是计算出来的.


返回值

值将根据其类返回:

  • MEMORY
    调用者将一个隐藏的第一个参数传递给函数,以便将结果存储到函数中.
    在C++中,这通常涉及复制省略/返回值优化.必须返回此地址eax,从而将"通过引用"的MEMORY类返回到隐藏的调用者分配的缓冲区.

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

  • INTEGERPOINTER
    寄存器raxrdx根据需要.

  • SSESSEUP 寄存器xmm0xmm1根据需要.

  • X87X87UP 寄存器st0


技术定义在这里.

ABI的定义如下.

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

   •其类没有虚函数,也没有虚基类,并且
   •其类的所有直接基类都具有简单的de/constructors,并且
   •对于类的所有非静态数据成员(类型或其数组) ,每个这样的类都有一个简单的de/constructor.


注意,每个8B被独立分类,以便可以相应地传递每个8B.
特别是,如果没有剩余的参数寄存器,它们可能最终在堆栈上.


Mar*_*ica 5

此处记录x86-64 ABI ,版本 252(根据我的回答是最新的 ABI)可在此处下载。

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

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

  • 每个类都有一个复制构造函数。关键点是“非平凡的复制构造函数或析构函数”。这就是为什么 `= default;` 很重要,以及为什么 `unique_ptr` 不是一个零成本的抽象。 (2认同)