在C中除以零和未定义的行为

时间:2013-08-19 01:26:01

标签: c language-lawyer undefined-behavior

this paper中,以下是一段可以触发除零的代码示例:

if (arg2 == 0)
    ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO),
                    errmsg("division by zero")));
/* No overflow is possible */
PG_RETURN_INT32((int32) arg1 / arg2);

ereport这是一个扩展为bool调用的宏 - 返回函数errstart,可能返回也可能不返回,有条件(使用?:)在其返回值上,调用另一个函数。在这种情况下,我认为ereportERROR无条件地在其他地方导致longjmp()

因此,对上述代码的简单解释是,如果arg2非零,则会发生除法并返回结果,而如果arg2为零,则会出现错误据报道,该部门不会发生。然而,链接的论文声称C编译器可以在零检查之前合法地提升除法,然后推断零检查从未被触发。他们唯一的推理似乎是错误的,那就是

  

[T]程序员未能通知编译器对ereport(ERROR,:: :)的调用没有返回。这意味着除法将始终执行。

John Regehr有simpler example

void bar (void);
int a;
void foo3 (unsigned y, unsigned z)
{
  bar();
  a = y%z;
}

根据这篇博客文章,clang在调用bar之上提升模运算,并显示了一些汇编代码来证明它。

我对C的理解适用于这些片段是

  1. 没有或不可以返回的函数在标准C中是格式良好的,并且这样的声明不需要特定的属性,铃声或哨声。

  2. 对函数的调用语义没有或没有返回,这种语义是明确定义的,特别是在C99中6.5.2.2“函数调用”。

  3. 由于ereport调用是完整表达式,因此;处有一个序列点。同样,由于John Regehr代码中的bar调用是完整表达式,因此;处有一个序列点。

  4. 因此ereport调用或bar调用之间存在一个序列点 和分裂或模数。

  5. C编译器可能不会向未自行引发未定义行为的程序引入未定义的行为。

  6. 这五点似乎足以得出结论:上面的除零测试是正确编写的,并且在bar的调用之上提升模数是不正确的。两个编译器和许多专家不同意。我的推理出了什么问题?

2 个答案:

答案 0 :(得分:8)

论文是错误的,至于clang示例,这是一个编译器错误(clang的一个相当常见的情况......)。我希望我能给你更好的理由,但你已经在问题中提供了所有正确的推理。

实际上,对于clang问题,据我所知,还没有出现任何错误。由于您链接到的博客上的示例中bar 确实返回,因此编译器可以在整个调用中自由重新排序。如果bar在同一个翻译单元中定义,那么这很简单,但LTO也是如此。要真正测试此错误,您需要一个永不返回的函数bar

答案 1 :(得分:2)

使用"好像"规则...

可以在编译器感觉到的地方完成除法;只要生成的代码就像在正确的位置完成除法一样。

这意味着:

a)如果除以零(或导致溢出的除法)导致异常(例如,典型的80x86上的整数除法),那么除法不能在函数调用之前完成(除非编译器可以证明分裂总是安全的。)

b)如果除以零(或导致溢出的除法)不会导致异常(例如80x86上的浮点典型),编译器可以在函数调用之前进行除法;只要被调用的函数中没有任何内容可以修改除法中使用的值。