假设 Linux 上的 x86-64 ABI,在 C++ 中的什么条件下,结构传递给函数是在寄存器中还是在堆栈中?在什么情况下它们会返回到寄存器中?类(class)的答案会改变吗?
如果它有助于简化答案,您可以假设一个参数/返回值并且没有浮点值。
最佳答案
我假设读者已经习惯了文档的术语,并且他们可以对基本类型进行分类。
如果对象大小大于两个八字节,则在内存中传递:
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;
}
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
是整数,所以第二个 8B 是整数。
最终的类是计算出来的类。
返回值
值相应地返回给它们的类:
内存
调用者将一个隐藏的第一个参数传递给函数,以便将结果存储到其中。
在 C++ 中,这通常涉及复制省略/返回值优化。 此地址必须返回到eax
中,从而将 MEMORY 类“通过引用”返回到隐藏的调用者分配的缓冲区。If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument. On return %rax will contain the address that has been passed in by the caller in %rdi.
整数 和指针
根据需要注册rax
和rdx
。SSE 和 SSEUP 根据需要注册
xmm0
和xmm1
。X87 和 X87UP 寄存器
st0
POD
技术定义是here .
ABI 的定义报告如下。
A de/constructor is trivial if it is an implicitly-declared default de/constructor and if:
• its class has no virtual functions and no virtual base classes, and
• all the direct base classes of its class have trivial de/constructors, and
• for all the nonstatic data members of its class that are of class type (or array thereof), each such class has a trivial de/constructor.
注意每个8B都是独立分类的,这样每个8B都可以相应的通过。
特别是,如果没有更多的参数寄存器,它们可能最终会进入堆栈。
关于x86-64 : when are structs/classes passed and returned in registers? 上的 C++,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42411819/