GCC 如何实现变长数组?

Posted

技术标签:

【中文标题】GCC 如何实现变长数组?【英文标题】:How does GCC implement variable-length arrays? 【发布时间】:2014-02-06 13:42:54 【问题描述】:

GCC 如何实现可变长度数组 (VLA)?这样的数组本质上是指向alloca返回的动态分配存储的指针吗?

我能想到的另一种选择是,将这样的数组分配为函数中的最后一个变量,以便在编译时知道变量的偏移量。但是,第二个 VLA 的偏移量将在编译时再次未知。

【问题讨论】:

VLA works by placing the array in the stack - ***.com/questions/2034712/variable-length-arrays。这也是我在使用 VLA 时检查 gcc 生成的程序集输出时看到的,没有调用 malloc。但这可能取决于实际的实现。 这是一个开源项目。你可以阅读代码。或者,您可以简单地通过检查它省略的代码来解决它。另请注意,完全有可能在不同的平台上有不同的实现。 实现使用malloc 来实现VLA 没有任何意义,因为malloc 可能会失败。如果有足够的可用堆栈空间,则可以保证分配 VLA 成功。 malloc 永远不能保证成功。 @Brandin:分配可变长度数组或通过malloc 分配都不能保证无限期地工作。在最常见的 C 实现中,使用malloc 处理可变长度数组将支持比使用堆栈更大的可变长度数组,因为可用于动态分配的空间远大于默认堆栈大小。 @Brandin:除此之外,大多数 C 实现不提供任何关于堆栈空间例程将使用多少的保证,不提供任何帮助来检查编译结果以查看它们有多少使用,并且不支持运行时检查已使用了多少堆栈空间(尽管显然可以将堆栈指针的值与堆栈限制进行比较,前提是已经研究了实现并使用了非标准代码)。因此,没有支持的方法来防止创建可变长度数组的灾难性失败。程序简单地中止。 【参考方案1】:

好吧,根据 VLA 的限制,这些只是黑暗中的一些狂野刺,但无论如何:

VLA 不能是:

外部 结构成员 静态 未指定边界声明(保存为函数原型)

所有这些都表明 VLA 被分配在 堆栈 上,而不是堆上。所以是的,VLA 可能是分配新块时分配的最后一块堆栈内存(块在块范围中,这些是循环、函数、分支或其他)。 这也是 VLA 会增加堆栈溢出风险的原因,在某些情况下会显着增加(警告:甚至不要考虑将 VLA 与递归函数调用结合使用!)。 这也是越界访问很可能导致问题的原因:一旦块结束,任何指向 Was VLA 内存的东西都指向无效内存。 但是有利的一面:这也是为什么这些数组是线程安全的(因为线程有自己的堆栈),以及为什么它们比堆内存更快。

VLA 的大小不能是:

extern 值 零或负数

extern 限制是不言而喻的,非零非负限制也是如此……但是:如果指定 VLA 大小的变量是有符号整数,例如,编译器不会产生错误:VLA 的评估和分配是在运行时完成的,而不是在编译时完成的。因此,VLA 的大小不能,也不必在编译时给定。 正如MichaelBurr 正确指出的那样,VLA 与alloca 内存非常相似,恕我直言,关键区别之一是:alloca 分配的内存从分配点开始有效,并贯穿函数的其余部分。 VLA 是块作用域的,因此一旦退出使用 VLA 的块,内存就会被释放:

void alloca_diff( void )

    char *alloca_c, *vla_c;
    for (int i=1;i<10;++i)
    
        char *alloca_mem = alloca(i*sizeof(*alloca_mem));
        alloca_c = alloca_mem;//valid
        char vla_arr[i];
        vla_c = vla_arr;//invalid
    //end of scope, VLA memory is freed
    printf("alloca: %c\n", *alloca_c);//fine
    printf("vla: %c\n\", *vla_c);//undefined behaviour... avoid!
//end of function alloca memory is freed, irrespective of block scope

【讨论】:

【参考方案2】:

这是取自some GCC docs for VLA support的以下示例行的分配代码(x86 - x64 代码类似):

char str[strlen (s1) + strlen (s2) + 1];

strlen (s1) + strlen (s2) + 1 的计算在 eax 中(GCC MinGW 4.8.1 - 无优化):

mov edx, eax
sub edx, 1
mov DWORD PTR [ebp-12], edx
mov edx, 16
sub edx, 1
add eax, edx
mov ecx, 16
mov edx, 0
div ecx
imul    eax, eax, 16
call    ___chkstk_ms
sub esp, eax
lea eax, [esp+8]
add eax, 0
mov DWORD PTR [ebp-16], eax

所以它看起来本质上是alloca()

【讨论】:

我认为 alloca 是模拟 VLA 支持的 "fix",但有一些细微的差别,比如 VLA 是按块进行 GC,alloca 内存按功能 @EliasVanOotegem:我认为 alloca() 早于 VLA 支持。此外,alloca() 在函数返回时被释放,例如,循环中的alloca() 调用将增大堆栈。循环中的 VLA 将在每次迭代中被释放和重新分配。此外,正如 VLA 的 GCC 文档页面中所述,alloca() 和 VLA 不一定能很好地配合使用(释放 VLA 可以在函数返回之前“释放”alloca() 分配)。 我想知道当 -fomit-frame-pointer 被传递时这是如何工作的 @HansPassant:我猜 MinGW 的人在他们的库中添加了一个 __chkskk_ms 函数来支持相同的功能(并允许将 MSVC 生成的目标文件链接到 MinGW 运行时)。 这是未优化的代码。没有理智的编译器会使用 divimul 来获得 2 的编译时间常数幂,除非有时禁用优化。这只是因为 GCC 的 VLA 的固定操作序列在 处理编译时常量的通常转换传递之后被替换。显然它是用x / 16 * 16 而不是x &amp;= -16 写成的,以向下舍入到下一个16 的倍数。

以上是关于GCC 如何实现变长数组?的主要内容,如果未能解决你的问题,请参考以下文章

GCC手册解析——变长数组

在编译时避免变长堆栈数组

变长数组的原型

如何使用基数排序对变长字符串数组进行排序?

关于如何定义一个未知大小的数组

通过自定义的 Array 类来实现变长数组