13

本文中,下面是一段可以触发除零的代码示例:

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-returning 函数的调用,并且在其返回值上有条件(使用 a )调用另一个函数。在这种情况下,我相信水平会无条件地导致其他地方。errstart?:ereportERRORlongjmp()

因此,对上述代码的一个天真的解释是,如果arg2为非零,则进行除法并返回结果,而如果arg2为零,则报告错误并且不进行除法。但是,链接的论文声称 C 编译器可能会在零检查之前合法地提升除法,然后推断永远不会触发零检查。他们唯一的推理,在我看来是不正确的,是

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

John Regehr 有一个更简单的例子

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调用是一个完整的表达式,因此在;. 同样,由于barJohn Regehr 代码中的调用是一个完整的表达式,因此在;.

  4. 因此在ereport调用或bar调用与除法或模数之间存在一个序列点。

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

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

4

2 回答 2

8

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

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

于 2013-08-19T01:36:12.327 回答
2

使用“好像”规则...

可以在编译器喜欢的任何地方进行除法;只要生成的代码表现得好像除法是在正确的位置完成的。

这意味着:

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

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

于 2014-11-11T04:29:49.100 回答