在Linux上假设使用x86-64 ABI,C ++中的struct何时通过寄存器传递给函数而不是放置在栈上?它们何时通过寄存器返回?这个答案对类是否有所改变呢?
如果简化回答有助于理解,可以假设只有一个参数/返回值并且没有浮点值。
在Linux上假设使用x86-64 ABI,C ++中的struct何时通过寄存器传递给函数而不是放置在栈上?它们何时通过寄存器返回?这个答案对类是否有所改变呢?
如果简化回答有助于理解,可以假设只有一个参数/返回值并且没有浮点值。
我假定读者习惯于文档术语,并且能够对原始类型进行分类。
如果对象大小大于两个八字节,则在内存中传递:
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]
}
如果以上条件都不成立,则会考虑对象的字段。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;
}
8个字节和它们对应的类型
第一个8B:a
第二个8B:b
a
是整数类型,所以第一个8B是整数类型。
b
是X87和X87UP类型,所以第二个8B是存储器类型。
最终类别对于这两个8Bs都是存储器类型。
例子
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。
最终的类别是计算得出的。
根据它们的类别相应地返回值:
MEMORY
调用者向函数传递一个隐藏的第一个参数,以将结果存储在其中。
在C++中,这通常涉及复制省略/返回值优化。
必须将此地址返回到eax
,从而将MEMORY类“按引用”返回给隐藏的调用者分配的缓冲区。
如果类型具有MEMORY类,则调用者提供返回值的空间并将此存储器的地址作为第一个参数(就好像它是函数的第一个参数)。 实际上,此地址成为“隐藏”的第一个参数。 在返回时,%rax将包含由调用者在%rdi中传入的地址。
INTEGER和POINTER
需要使用寄存器rax
和rdx
。
SSE和SSEUP
需要使用寄存器xmm0
和xmm1
。
X87和X87UP
需要使用寄存器st0
技术定义在这里。
来自ABI的定义如下。
如果它是一个隐式声明的默认de/constructor,并且:
• 它的类没有虚函数和虚基类,且
• 其类的所有直接基类具有微不足道的de/constructors,以及
• 对于其类的所有非静态数据成员,其类型为类类型(或其数组),那么该类的每个这样的类都具有微不足道的de/constructor。
注意,每个8B都独立分类,因此可以相应地传递每个8B。
特别是,如果没有更多的参数寄存器,则它们可能最终位于堆栈上。
= default;
很重要,以及为什么unique_ptr
不是零成本抽象的原因。 - Kerrek SB