在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
,可能返回也可能不返回,有条件(使用?:
)在其返回值上,调用另一个函数。在这种情况下,我认为ereport
级ERROR
无条件地在其他地方导致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的理解适用于这些片段是
没有或不可以返回的函数在标准C中是格式良好的,并且这样的声明不需要特定的属性,铃声或哨声。
对函数的调用语义没有或没有返回,这种语义是明确定义的,特别是在C99中6.5.2.2“函数调用”。
由于ereport
调用是完整表达式,因此;
处有一个序列点。同样,由于John Regehr代码中的bar
调用是完整表达式,因此;
处有一个序列点。
因此ereport
调用或bar
调用之间存在一个序列点
和分裂或模数。
C编译器可能不会向未自行引发未定义行为的程序引入未定义的行为。
这五点似乎足以得出结论:上面的除零测试是正确编写的,并且在bar
的调用之上提升模数是不正确的。两个编译器和许多专家不同意。我的推理出了什么问题?
答案 0 :(得分:8)
论文是错误的,至于clang示例,这是一个编译器错误(clang的一个相当常见的情况......)。我希望我能给你更好的理由,但你已经在问题中提供了所有正确的推理。
实际上,对于clang问题,据我所知,还没有出现任何错误。由于您链接到的博客上的示例中bar
确实返回,因此编译器可以在整个调用中自由重新排序。如果bar
在同一个翻译单元中定义,那么这很简单,但LTO也是如此。要真正测试此错误,您需要一个永不返回的函数bar
。
答案 1 :(得分:2)
使用"好像"规则...
可以在编译器感觉到的地方完成除法;只要生成的代码就像在正确的位置完成除法一样。
这意味着:
a)如果除以零(或导致溢出的除法)导致异常(例如,典型的80x86上的整数除法),那么除法不能在函数调用之前完成(除非编译器可以证明分裂总是安全的。)
b)如果除以零(或导致溢出的除法)不会导致异常(例如80x86上的浮点典型),编译器可以在函数调用之前进行除法;只要被调用的函数中没有任何内容可以修改除法中使用的值。