C"可观察到的行为"在UB"未定义的行为的背景下"

时间:2016-01-16 04:15:18

标签: c language-lawyer

(问题最初是根据Are there race conditions in this producer-consumer implementation?的答案提出的,但是这里严格地从C语言的角度提出问题,没有涉及任何并发或多线程。)

考虑这个最小的代码:

#define BUFSIZ 10
char buf[BUFSIZ];

void f(int *pn)
{
    buf[*pn]++;
    *pn = (*pn + 1) % BUFSIZ;
}

int main()
{
    int n = 0;
    f(&n);
    return n; 
}

问题:C " as-if" 规则是否允许编译器重写代码如下?

void f(int *pn)
{
    int n = *pn;
    *pn = (*pn + 1) % BUFSIZ;
    buf[n]++;
}

一方面,上述内容不会改变程序的可观察行为。

另一方面,可能使用无效索引调用f,可能来自另一个翻译单元:

int g()
{
    int n = -1001;
    f(&n);
}

在后一种情况下,代码的两种变体在访问越界数组元素时都会调用UB。但是,原始代码会将*pn保留为传递到f(= -1001)的值,而重写的代码只有在修改*pn后才会进入UB-land(到{{ 1}})。

这种差异是否会被视为"可观察"或者,回到实际问题,C标准中是否有任何特定允许或排除此类代码重写/优化的内容?

1 个答案:

答案 0 :(得分:5)

  1. 如果程序的任何部分有未定义的行为,则整个程序的行为是未定义的。换句话说,即使在“行为未定义”的任何构造“之前”,程序的行为也是未定义的。 (这是允许编译器执行某些优化的必要条件,这些优化取决于所定义的行为。)

  2. 鉴于这两个变量都没有声明为volatile,我相信内存更新的顺序可能会按照指示进行重新排序,因为只有在没有undefined的情况下才能保证可观察行为符合执行模型行为。

  3. “可观察行为”(标准C中)在§5.1.2.3中定义为:

      
        
    • 严格按照抽象机的规则评估对易失性对象的访问。
    •   
    • 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序的结果相同。
    •   
    • 交互设备的输入和输出动态应按照7.21.3的规定进行。这些要求的目的是尽快出现无缓冲或行缓冲输出,以确保提示消息实际出现在   一个等待输入的程序。
    •   

    此列表不包括对未定义行为(例如陷阱或信号)的任何潜在响应,即使用本地术语表示段错误通常是可观察的。问题中的特定示例不涉及这三点中的任何一点。 (UB可以阻止程序成功终止,这基本上使可观察行为中的第二点无效。)因此,在问题中代码的特定情况下,重新排序不会改变任何可观察的行为并且可以清楚地执行。

  4. 我声明实现对未定义行为的响应不仅限于严格执行导致未定义行为的组件,而是在注释线程中引起了比我预期更多的争议,因为它是一个相当着名的特性在essay undefined behaviour {} {{}} {}} {}} {/}>

      

    更具体地说,当程序因执行非法操作(例如除以零或取消引用空指针)而死亡时,这被认为是副作用吗?答案肯定是“不”。...由于崩溃诱导操作不会产生副作用,编译器可以根据其他操作对它们进行重新排序,

    作为一个可能更有趣的例子(取自评论线程),如果一个程序产生几行输出,然后故意执行一个显式的除零,人们可能会期望编译和运行该程序会产生在以任何未定义的方式响应之前输出它响应除零。但是,检测到被零除的并且可以证明程序的控制流保证其执行的编译器将完全有权在转换时生成错误消息 并且拒绝生成可执行文件图像。

    或者,如果它不能证明控制流程达到零除,它就有权假设零分割不会发生,因此删除所有明确导致分割的代码 - by-zero(包括对输出函数的调用)作为死代码。

    上述两个内容都符合§3.4.3中未定义行为的示例响应列表:“完全忽略不可预测的结果,...终止翻译或执行(发出诊断消息)”。 “