为什么 GCC 和 Clang 使用可变长度数组产生不同的输出?

Eri*_*hil 7 c

为什么 GCC 和 Clang 使用此一致的 C 代码产生不同的输出:

int (puts) (); int (main) (main, puts) int main;
char *puts[(&puts) (&main["\0April 1"])]; <%%>
Run Code Online (Sandbox Code Playgroud)

即使使用 ,编译器也不会产生任何警告或错误,但使用 GCC 构建时,-Wall -std=c18 -pedantic程序不会产生任何输出,但使用 Clang 构建时,会打印当前日期

Joh*_*ger 11

为什么 GCC 和 Clang 使用此一致的 C 代码产生不同的输出:

int (puts) (); int (main) (main, puts) int main;
char *puts[(&puts) (&main["\0April 1"])]; <%%>
Run Code Online (Sandbox Code Playgroud)

首先,它一致的代码,尽管它确实使用了可变长度数组,这是 C11 和 C17 中的可选语言功能。一些混淆是

  • 使用晦涩的二合字母<%和,其含义与和%>相同{}相同。
  • 在函数声明中将函数标识符括起来
  • puts非原型函数的前向声明
  • K&R 风格的函数定义main
    • 带 VLA 参数
      • 其维度表达式包含函数调用
      • 以及对另一个参数的引用
  • 使用非常规标识符来运行参数main()
  • 分别在具有相同标识符的对象和函数的声明中使用标识符 (puts和)main
  • 将标识符用于main程序入口点函数之外的其他用途
  • []索引运算符 ( ) 的操作数的常规顺序反转
    • 另外,索引字符串文字
  • 通过显式函数指针常量表达式调用函数
  • 包含显式空字符的字符串文字
  • 换行符的非常规放置(和省略)

一个不太混乱的等价物是

int puts();

int main(
    int argc,
    char *argv[ puts("\0April 1" + argc) ]
) {
}
Run Code Online (Sandbox Code Playgroud)

但是,关于使用 GCC 编译的版本和使用 Clang 构建的版本之间行为差异的核心问题在于 VLA 函数参数大小的表达式是否在运行时计算。

语言规范规定,当使用数组类型声明函数参数时,其类型将“调整”为相应的指针类型。这同样适用于完整、不完整和可变长度数组类型,但规范没有明确说明不计算维度的表达式。它确实指定在某些其他情况下不评估表达式,甚至在涉及 VLA 的表达式的情况下对此类规则进行例外处理sizeof,因此这种情况下的省略可以解释为有意义。

这仅对 VLA 类型的参数产生影响,因为只有对那些维度表达式的求值才能对机器状态产生副作用,包括但不限于可观察的程序行为。

GCC 不会在运行时计算 VLA 参数的大小表达式,我倾向于认为这符合标准的意图。结果,GCC 编译的程序除了以状态 0 退出之外什么也不做。

Clang 在运行时评估 VLA 参数的大小表达式。尽管我不赞成对规范的这种解释,但我不能排除它。当它计算大小表达式时,它使用传递的第一个参数的值。当程序不带参数运行时,第一个参数的值为 1,结果是puts使用指向'A'in的指针调用标准库的函数"\0April 1"

  • 有趣的是,如果使用现代风格的参数声明,GCC 还会评估大小。 (2认同)