在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
和?:
水平无条件地导致aereport
其他地方。因此,对上述代码的简单解释是,如果
ERROR
为非零,则会发生除法并返回结果;而如果longjmp()
为零,则会报告错误且不会发生除法。然而,链接的文章声称,C编译器可以在零检查之前合法地提升除法,然后推断零检查从未被触发。他们唯一的理由,在我看来是不正确的,就是程序员未能通知编译器对eReport的调用(错误:::)不会返回。这意味着该部门将始终执行。
John Regehr有一个simpler example:
void bar (void);
int a;
void foo3 (unsigned y, unsigned z)
{
bar();
a = y%z;
}
根据这篇博客文章,clang将模块操作提升到调用
arg2
之上,并展示了一些汇编代码来证明这一点。我对C语言的理解是
不返回或可能不返回的函数在标准C中格式良好,并且此类函数的声明不需要特定的属性、钟声或哨声。
对不返回或可能不返回的函数的调用的语义定义良好,特别是在c99中的6.5.2.2“函数调用”中。
因为
arg2
调用是一个完整的表达式,所以在bar
处有一个序列点。类似地,由于john regehr代码中的ereport
调用是一个完整的表达式,因此在;
处有一个序列点。因此,在
bar
调用或;
调用之间有一个序列点以及除法或模。
C编译器可能不会将未定义的行为引入那些本身不会引发未定义行为的程序。
这五点似乎足以得出这样的结论:上面的零除测试是正确编写的,将模提升到调用
ereport
之上是不正确的。两位编撰者和许多专家意见不一致。我的推理怎么了? 最佳答案
论文是错误的,至于clang示例,这是一个编译器错误(clang中很常见的一个错误…)。我希望我能给你更好的理由,但你已经在这个问题上提供了所有正确的理由。
事实上,对于clang问题,据我所知,还没有出现bug。因为bar
在您链接到的blog上的示例中确实返回,所以编译器可以在整个调用中对除法重新排序。如果bar
是在同一个翻译单元中定义的,那么这一点很简单,但是使用lto也是可能的。要真正测试这个bug,需要一个永远不会返回的函数bar
。