UB上下文中的“可观察到的行为”“未定义行为”

dxiv

(问题最初是在“此生产者-消费者实现中是否存在竞争条件?”这个答案的注释下提出的,但这里严格是从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的“假设”规则是否允许编译器按以下方式重写代码?

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. §5.1.2.3中将“可观察的行为”(在标准C中)定义为:

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

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

  4. 我的声明,即实现对未定义行为的响应不仅仅限于严格执行导致未定义行为的组件,这在注释线程中引起了比我预期更多的争议,因为这是现代C的一个众所周知的功能。值得回顾约翰·雷格(John Regehr)关于未定义行为的有用文章,在此我引用:(第三部分)

    更具体地,当程序由于执行诸如除零或取消引用空指针之类的非法操作而死时,是否认为这是副作用?答案肯定是“不”。…由于引发崩溃的操作没有副作用,因此编译器可以针对其他操作对它们进行重新排序,

    作为一个可能更有趣的示例(来自注释线程),如果程序产生多行输出,然后有意执行显式除零,则可能会期望编译和运行程序会在响应之前产生输出它以任何不确定的方式响应被零除。但是,编译器检测到零除并可以证明保证程序执行的控制流将完全有资格在翻译时生成错误消息并拒绝生成可执行映像。

    或者,如果不能证明控制流达到零除,则有权假定不能进行零除,因此可以明确删除导致零除的所有代码。 (包括对输出函数的调用)作为无效代码。

    上面的两种方法都适用于第3.4.3节中对未定义行为的示例响应列表:“从完全以无法预测的结果忽略情况,到终止转换或执行(伴随发出诊断消息)。”

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章