根据AMD64 ABI,什么样的C11数据类型是阵列

时间:2016-08-06 02:33:33

标签: c assembly types x86-64 calling-convention

我正在研究在OSX上使用的x86_64的调用约定,并且正在阅读名为" Aggregates and Unions"在the System V x86-64 ABI standard)。它提到了数组,我认为这就像一个固定长度的c数组,例如int[5]

我去了" 3.2.3参数传递"阅读如何通过数组,如果我正确理解,uint8_t[3]之类的内容应该通过寄存器传递,因为它小于聚合分类规则1规定的四个八字节限制类型(靠近底部的第18页)。

编译后我看到它被作为指针传递。 (我在OSX 10.11.6上使用Xcode 7.3.1中的clang-703.0.31进行编译)。

我用来编译的示例源如下:

#include <stdio.h>

#define type char

extern void doit(const type[3]);
extern void doitt(const type[5]);
extern void doittt(const type[16]);
extern void doitttt(const type[32]);
extern void doittttt(const type[40]);

int main(int argc, const char *argv[]) {
  const char a[3] = { 1, 2, 3 };
  const char b[5] = { 1, 2, 3, 4, 5 };
  const char c[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 1, 1, 1, 1 };
  const char d[32] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 1, 1, 1, 1 };
  const char e[40] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };

  doit(a);
  doitt(b);
  doittt(c);
  doitttt(d);
  doittttt(e);
}

我将其转储到名为a.c的文件中,并使用以下命令编译:{{1​​}}。我使用otool分析生成的程序集(通过运行clang -c a.c -o a.o)并获得以下输出:

otool -tV a.o

或者等效地,它位于Godbolt compiler explorer with clang3.7上,它使用相同的ABI作为Linux。

所以,我想知道是否有人可以引导我将C11中的哪些数据类型应用于数组。 (看起来clang默认使用C11 - 请参阅C99内联函数下的blurb here。)

我也对ARM进行了类似的调查并发现了类似的结果,即使 ARM standard也指定存在阵列聚合类型

另外,在某些标准中是否存在指定固定长度数组被视为指针的地方?

1 个答案:

答案 0 :(得分:7)

Bare 数组作为C和C ++中的函数args 总是衰减到指针,就像在其他几个上下文中一样。

structunion内的数组不会,并按值传递。这就是为什么ABI需要关心它们如何通过的原因,即使它在C中没有发生裸阵列。

作为Keith Thomson points out,C标准的相关部分是N1570 section 6.7.6.3 paragraph 7

  

参数声明为“类型数组”应调整为   “限定指向类型的指针”,其中类型限定符(如果有)是   那些在数组类型派生的[和]中指定的...... (关于foo[static 10]的东西,见下文)

请注意,多维数组作为数组类型的数组工作,因此只有“array-ness”的最外层级别才会转换为指向数组类型的指针。

术语:x86-64 ABI doc使用与ARM相同的术语,其中struct和数组是“聚合”(连续地址处的多个元素)。因此,“聚合和联合”这个短语出现了很多,因为union的语言和ABI的处理方式类似。

这是处理复合类型(struct / union / class)的递归规则,它使ABI中的数组传递规则发挥作用。 这是你看到asm将数组作为函数arg的一部分复制到堆栈的唯一方法,对于C或C ++

struct s { int a[8]; };
void ext(struct s byval);

void foo() { struct s tmp = {{0}}; ext(tmp); }

gcc6.1 compiles it (for the AMD64 SysV ABI, with -O3)以下内容:

    sub     rsp, 40    # align the stack and leave room for `tmp` even though it's never stored?
    push    0
    push    0
    push    0
    push    0
    call    ext
    add     rsp, 72
    ret

在x86-64 ABI中,pass-by-value通过实际复制(进入寄存器或堆栈)而不是隐藏指针发生。

请注意,当返回值太大而无法适应rdi的128位串联时,按值返回会将指针作为“隐藏”的第一个arg(在rdx:rax中)传递(并且不是在向量regs等中返回的向量。)

ABI有可能使用隐藏指针来传递超过一定大小的值传递对象,并且相信被调用函数不会修改原始对象,但这不是x86-64 ABI选择做的事情。在某些情况下会更好(特别是对于没有修改(即浪费)的大量复制的低效C ++),但在其他情况下更糟糕。

SysV ABI奖金阅读:正如标签wiki指出的那样,当前版本的ABI标准并未完全记录编译器所依赖的行为:{{3} }。

请注意,要确保函数arg是固定大小的数组, clang/gcc sign/zero extend narrow args to 32bit :对于数组大小。 (当然,它仍然作为指针传递。这不会改变ABI)。

void bar(int arr[static 10]);

这使得sizeof(arr)可以在调用函数内部工作,并允许编译器警告超出范围。如果编译器知道允许访问C源不能访问的元素,它还可能实现更好的优化。 (见C99 and later lets you use the static keyword in a new way)。

this blog post表示ISO C ++不支持static的这种用法;它是C-only功能中的另一个,还有C99可变长度数组和C ++没有的其他一些好东西。

在C ++中,您可以使用The same keyword page for C++来获取传递给调用者的编译时大小信息。但是,如果这是你想要的,你必须通过引用手动传递它,因为它当然只是一个包含int arr[10]的类。 与C风格的数组不同,它不会自动衰减到T*

您链接的ARM文档似乎实际上并未将数组调用为聚合类型:第4.3节“复合类型”(讨论对齐)将数组与聚合类型区分开来,即使它们似乎是其聚合定义的特例。

  

复合类型是一个或多个基本数据类型的集合,它们作为单个实体处理   程序调用级别。复合类型可以是以下任何一种:

     
      
  • 聚合,其中成员按顺序排列在内存中
  •   
  • 一个联盟,其中每个成员具有相同的地址
  •   
  • 一个数组,是其他类型(基类型)的重复序列。
  •   
     

定义是递归的;也就是说,每种类型都可以包含一个复合类型作为成员

“复合”是一个包含数组,结构和联合的总称。