【问题标题】:understanding output of c code on gcc compiler理解 gcc 编译器上 c 代码的输出
【发布时间】:2015-08-12 02:22:27
【问题描述】:

我从几篇类似What are all the common undefined behaviours that a C++ programmer should know about? 的帖子中了解到,该行为未定义并留给编译器。

但是对于给定的代码,输出是2 1 3(总是,始终如一)

#include<stdio.h>
int main(){
    int i = 1;
    printf("%d %d %d\n", i++, i++, i);
    return 0;
}

所以,我想知道,gcc 遵循什么顺序,看起来不像从左到右或从右到左?输出总是相同的,这个问题只与 gcc 4.9.1 编译器有关。

【问题讨论】:

  • 如果你想知道 gcc 为这个特定的运行做了什么,请查看程序集输出(使用 -S 开关)。请记住,这可能随时发生变化,尤其是当您更改编译器开关或使用不同的编译器或在程序的其他任何地方进行更改时。
  • “始终如一”?真的吗?即使我们将自己限制为编译器的单个特定版本,您确定您测试了所有可能的编译器设置组合吗?周围环境的所有可能变体呢?如果你不这样做,你怎么能做出“始终如一”的主张?
  • 无论如何,是什么让您认为它是故意遵循某个命令,而不是出于计算机程序的原始自然原始决定论无意中产生相同的命令?
  • 我玩了一下这个。我将调用“A”“i++”和“B”“i”。所以printf("%d %d %d\n", i++, i++, i) 是“AAB”。好的? AAAB3214AAAAB 是 43215。AAABAB 是 432515。AAAABAABAB7654321818。规则似乎是从右到左执行所有操作,同时执行替换,然后替换任何变量。我没有任何野心去确定。
  • 我投票决定将此问题作为题外话结束,因为它要求仅对一个版本的 gcc 解释 UB。对于 SO 访问者,我认为这没有持久的价值。如果有人想调查 UB,他们应该自己做。

标签: c gcc output


【解决方案1】:

你指定了gcc,所以我会说一些非常具体的gcc

这是未定义的,但是如果您考虑 gcc 如何实现调用堆栈,您就会知道为什么它是一致的。基本上,堆栈从高地址向低地址增长,并且像printf 这样的函数在其参数列表中有... 会将参数转储到堆栈中。考虑到堆栈的方向,没有太多选择:

func(a1, a2, a3, a4);

从左向右推:

high| ... |
    | a1  |
    | a2  |
    | a3  |
low | a4  |

或从右到左:

high| ... |
    | a4  |
    | a3  |
    | a2  |
low | a1  |

事实上,gcc 采用了第二种方法。这听起来可能违反直觉,但考虑到这样的堆栈结构,这设计得非常好。如果你必须按顺序访问参数,你自然会这样组成一个数组,所以arr[0]arr[1]等每个都会对应一个正确的参数。

所以在知道这一点之后,评估顺序不再难以理解,因为gcc 以任何方式处理从右到左的参数。如果我需要实现一个编译器处理函数调用,为什么我会选择在这种情况下从左到右处理参数,在这种情况下从右到左处理?这不仅令人困惑。

gcc 是一个非常全面的编译器,如果你知道它的一些实现的话。技术决策通常会导致非常明显的后果。

这是非常 gcc 特定的,所以如果有人改变堆栈的增长方向或者只是想创建特殊情况,事情可能会很容易改变。例如,clang 的行为似乎相反。

【讨论】:

  • 我喜欢这个答案,但这在其他任何地方的文档中都很明显吗?
  • @VermillionAzure 我现在无法深入研究文档林,但我可以清楚地告知CSAPP 引入的正是gcc 实现的构造。
  • 我喜欢你的知识,但缺乏引用具体内容的能力对 GCC 来说是一种耻辱:(
  • @VermillionAzure 如果有机会,我明天会尝试挖掘。睡前看太多文档不太好。
  • 你能给个链接,让我们开始挖掘吗?
【解决方案2】:

未定义的行为意味着一个编译器编译它的方式可能与另一个编译器不同。这是因为没有标准

关于 gcc 4.9.1 编译器,它正在评估中间的编译器,然后是左侧的编译器,然后是右侧的编译器。它像兔子一样跳来跳去。一般情况下可能是从中间到左到右。

【讨论】:

  • 我打赌添加另一个 i++ 将打印 3 2 1 4 另一个将打印 4 3 2 1 5
  • 是的,或者在后一种情况下,甚至是3 2 1 4 5
  • 另一种可能性是从右到左评估变异参数,然后评估非变异参数。确实有很多方法可以实现这种行为。
  • 这是最“全面”的UB。我会解释的。
  • 嗯,如果有人想要扩展行为的示例,我在Coliru 上做了一个更长的链。在所有警告之后,它确实打印了543216 模式
【解决方案3】:

GCC doesn't appear to have an extensive and comprehensive database or documentation concerning undefined behavior--如果有的话,似乎普遍认为不要冒险进入未定义的领域。但是,它确实有关于实现定义的行为的注释。

需要明确的是,我不认为 i = i++ 问题可以在同一编译器的平台和实现之间实现最佳标准化,因为跨架构的调用和计算语义不同:@987654322 @ 可能相关或有助于阐明架构如何具有特殊的调用约定。

此外,即使是 C++ 标准也允许一些“编译器室”允许他们通过对程序员施加限制以这种方式进行优化,尽管它们通常非常小且深奥,但可能很重要,例如 strict aliasing .

无论您如何将它们放入表达式中,固有冲突的单个表达式的执行顺序可能适合“编译器空间”,通常假定编译器对此具有完全控制权。

另外,由于 GCC 代码库的年代久远和数量庞大,我怀疑这种文档是否还会存在。

**编辑:用户HuStmpHrrr 似乎掌握了 GCC 的实现方式。看起来需要一些挖掘!

【讨论】:

    【解决方案4】:

    您可以要求 GCC 生成汇编代码:

    gcc -S -o my_asm_output.s main.c
    

    我明白了:

    call    ___main
    movl    $1, 28(%esp)
    movl    28(%esp), %edx
    leal    1(%edx), %eax
    movl    %eax, 28(%esp)
    movl    28(%esp), %eax
    leal    1(%eax), %ecx
    movl    %ecx, 28(%esp)
    movl    28(%esp), %ecx
    movl    %ecx, 12(%esp)
    movl    %edx, 8(%esp)
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    

    其中 LC0:

    LC0:
        .ascii "%d %d %d\12\0"
        .text
        .globl  _main
        .def    _main;  .scl    2;  .type   32; .endef
    

    我不是组装专家,但看起来打印顺序将是printf("eax edx ecx\n");,对应增量:

    movl    $1, 28(%esp)
    movl    28(%esp), %edx
    leal    1(%edx), %eax
    leal    1(%eax), %ecx
    

    但这只是一个快速的猜测,也许我跳过了一些重要的事情。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-22
      • 2023-03-18
      • 1970-01-01
      • 2015-12-01
      • 1970-01-01
      • 2020-01-31
      • 2014-09-09
      • 1970-01-01
      相关资源
      最近更新 更多