UB“未定义行为”背景下的C“可观察行为”

(问题最初是由这个答案中的评论引起的。这个生产者 – 消费者实现中是否存在竞争条件?但是这里严格地从C语言角度提出这个问题,没有涉及任何并发或multithreading。)

考虑这个最小代码:

#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 (为0 )后才会进入UB-land。

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

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

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

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

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

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

  4. 我的声明表明,实现对未定义行为的响应并不局限于严格遵循导致未定义行为的组件的执行,这在评论线程中引起了比我预期更多的争议,因为它是现代C的一个相当着名的特征。值得回顾John Regehr关于未定义 行为的有用论文 ,我引用它:(在第三部分)

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

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

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

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