【问题标题】:What kind of C11 data type is an array according to the AMD64 ABI根据AMD64 ABI,数组是什么C11数据类型
【发布时间】:2016-08-06 02:33:32
【问题描述】:

我正在研究在 OSX 上使用的 x86_64 的调用约定,并且正在阅读 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 的文件中,并使用以下命令进行编译:clang -c a.c -o a.o。我使用 otool 分析生成的程序集(通过运行otool -tV a.o)并得到以下输出:

a.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    subq    $0x10, %rsp
0000000000000008    leaq    _main.a(%rip), %rax
000000000000000f    movl    %edi, -0x4(%rbp)
0000000000000012    movq    %rsi, -0x10(%rbp)
0000000000000016    movq    %rax, %rdi
0000000000000019    callq   _doit
000000000000001e    leaq    _main.b(%rip), %rdi
0000000000000025    callq   _doitt
000000000000002a    leaq    _main.c(%rip), %rdi
0000000000000031    callq   _doittt
0000000000000036    leaq    _main.d(%rip), %rdi
000000000000003d    callq   _doitttt
0000000000000042    leaq    _main.e(%rip), %rdi
0000000000000049    callq   _doittttt
000000000000004e    xorl    %eax, %eax
0000000000000050    addq    $0x10, %rsp
0000000000000054    popq    %rbp
0000000000000055    retq

或者等效地,这里是Godbolt compiler explorer with clang3.7,它针对使用相同 ABI 的 Linux。


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

我也对 ARM 进行了类似的调查,发现了类似的结果,尽管 ARM standard 也指定存在数组聚合类型

另外,在某些标准中是否有规定将固定长度的数组视为指针?

【问题讨论】:

  • @PeterCordes:在大多数但不是所有上下文中,数组都会衰减为指针。更重要的是,C 不允许数组类型的参数。在像void func(int param[]); 这样的声明中,param 的类型从int[]int*调整。 (这与将数组表达式隐式转换为指针的规则不同。)
  • @PeterCordes 那么在 c11 或 c99 中是否没有数据类型被视为 x86_64/arm 标准中定义的数组?
  • @KeithThompson 你有关于 C 不允许数组类型参数的参考吗?我很想阅读更多内容!
  • @DanZimm:C 当然有数组,我认为它们对应于 x64_64/arm 标准中定义的“数组”。参考:N1570 第 6.7.6.3 节第 7 段。
  • @PeterCordes:“衰减”是一个常用术语,尽管标准没有这么称呼它;它只是说它是“转换的”。请注意,衰变并非特定于函数参数。除了 3 个上下文(sizeof&amp; 和用于初始化数组对象的初始化程序中的字符串文字),数组表达式被隐式转换为指针;参数传递恰好是发生转换的上下文之一。

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


【解决方案1】:

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

structs 或 unions 内的数组不会,而是按值传递。这就是为什么 ABI 需要关心它们是如何传递的,即使在 C 语言中对于裸数组而言不会发生这种情况。


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

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

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


术语:x86-64 ABI 文档使用与 ARM 相同的术语,其中structs 和数组是“聚合”(多个元素位于顺序地址)。所以“聚合和​​联合”这个短语出现了很多,因为unions 由语言和 ABI 类似地处理。

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

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 中,按值传递是通过实际复制(到寄存器或堆栈中)发生的,而不是通过隐藏指针。

请注意,当返回值太大而无法容纳 rdx:rax 的 128 位串联时(并且不是在向量 regs 等中返回的向量等)

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

SysV ABI 奖励阅读:正如 标签 wiki 指出的那样,当前版本的 ABI 标准并未完整记录编译器所依赖的行为:clang/gcc sign/zero extend narrow args to 32bit


请注意,要真正保证函数 arg 是固定大小的数组,C99 and later lets you use the static keyword in a new way:关于数组大小。 (当然,它仍然作为指针传递。这不会改变 ABI)。

void bar(int arr[static 10]);

这允许编译器警告超出范围。如果编译器知道允许访问 C 源代码不允许访问的元素,它还可能实现更好的优化。 (见this blog post)。但是,arg 仍然具有类型 int*,而不是实际的数组,因此是 sizeof(arr) == sizeof(int*)

The same keyword page for C++表示ISO C++不支持static的这种用法;这是 C 独有的另一个特性,以及 C99 可变长度数组和 C++ 所没有的其他一些优点。

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


您链接的 ARM 文档 似乎实际上并未将数组称为聚合类型:第 4.3 节复合类型(讨论对齐)将数组与聚合类型区分开来,尽管它们似乎是聚合定义的一个特例。

复合类型是一个或多个基本数据类型的集合,它们在 过程调用层。复合类型可以是以下任何一种:

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

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

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

【讨论】:

  • x86-84 ABI 文档也调用 arrays 聚合:The classification of aggregate (structures and arrays) and union types works as follows: 所以不应该是 arrays(由 ABI 定义)应该能够也按值传递?附言感谢您的详细回答,尤其是关于 static 关键字的部分 - 我从来不知道!
  • @DanZimm:废话,你说得对。我没有仔细检查术语,因为我(现在仍然是)100% 确定我对 C 数组 args 实际发生的情况是正确的:它们作为指针传递。也许其他语言允许数组的值传递? ABI 文档中有一个 Fortran 部分。
  • 也许 Rust/Swift 可以利用这一点?我在这台计算机上没有 clang 源,所以我不记得在哪里,但是按值传递数组似乎是在底层编译器中实现的(我认为我的术语是错误的,但我看到代码可以正确地将数组映射到按值而不是按指针注册/入栈)。
  • @DanZimm:这对于在结构中传递数组是有意义的。在这种情况下,它是根据 C 规则按值传递的聚合的一部分,因此它会被复制到 regs / stack 。
  • @DanZimm:好的,感谢您发现我的术语错误。我想我已经把这个答案整理成有用且没有错误的东西。我愿意接受有关在此答案中说的其他有用的建议。 (我想在函数 args 中对数组到指针衰减/转换的语言规范引用会很好,但我真的不想去挖掘它。就我而言,这基本上是一个众所周知的事实。 )
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-11
  • 1970-01-01
  • 2010-10-04
  • 2012-05-16
  • 1970-01-01
相关资源
最近更新 更多