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

时间:2017-02-23 09:35:42

标签: c++ assembly x86-64 abi

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

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

2 个答案:

答案 0 :(得分:11)

ABI规范定义为here 有更新的版本here

我认为读者习惯于文档的术语,并且他们可以对原始类型进行分类。

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

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]
}

Copy 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]
}

如果以上都不是真的,则考虑对象的字段 如果其中一个字段本身是结构/类,则递归应用该过程 目标是对对象中的两个八字节(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;
}

然后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;
}

8Bs及其领域

First 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
}

8Bs及其领域

First 8B a 第二个8B b

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

最后的课程是计算出来的。

返回值

根据类别返回值:

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

      

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

  • INTEGER POINTER
    根据需要注册raxrdx

  • SSE SSEUP 根据需要注册xmm0xmm1

  • X87 X87UP 注册st0

的POD

技术定义为here

ABI的定义见下文。

  

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

,则它是微不足道的      

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

请注意,每个8B都是独立分类的,因此每个8B都可以相应地传递 特别是,如果没有剩余的参数寄存器,它们可能会在堆栈中结束。

答案 1 :(得分:4)

x86-64 ABI记录here版本252(我的答案中最新的ABI)可下载here

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

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