中毒的空引用和短路表达评估错误或UB?

时间:2018-02-28 00:18:30

标签: c++ boost undefined-behavior

在升级到gcc 7.2之后,我最近遇到了一个奇怪的C ++崩溃,可以使用以下简单的完整c ++ 11程序来演示:

#include <cassert>
#include <cstdio>

struct MyObject
{
    static MyObject & null_obj()
        { return *static_cast<MyObject*>    (nullptr); }

    operator bool()
    {
        return value != 0;
    }

    int value = 0;
};

int foo(MyObject & obj = MyObject::null_obj())
{
    if (&obj != &MyObject::null_obj() && obj)
        return 1;

    return 0;
}

int main(int argc, char * argv[])
{
    int result;

    if (argc == 1)
    {
        result = foo();
    }
    else
    {
        MyObject obj;
        obj.value = 1;

        result = foo(obj);
    }

    printf("%d", result);
}

可以使用以下命令构建程序:g ++ -std = c ++ 11 -O2

当没有参数执行时 - 程序采用顶部分支并在使用gcc6.1或更高版本构建时崩溃。使用gcc 5.5构建时,它不会崩溃。

添加虚拟参数时 - 程序采用第二个分支,不会按预期崩溃。在没有编译器优化的情况下构建时,程序也不会崩溃。

当评估foo()中的条件时,崩溃似乎发生了。 根据短路评估规则,当表达式的第一部分为假时,不应执行此代码路径中的第二个条件。

当使用更新的编译器(例如gcc 6.1或更高版本)时,为foo生成的汇编程序如下所示:

foo(MyObject&):
  mov edx, DWORD PTR [rdi]
  xor eax, eax
  test edx, edx
  setne al
  ret

崩溃的罪魁祸首是函数顶部的mov指令。 gcc 5.5的汇编程序看起来有点不同:

foo(MyObject&):
  test rdi, rdi
  je .L3
  mov edx, DWORD PTR [rdi]
  xor eax, eax
  test edx, edx
  setne al
  ret
.L3:
  xor eax, eax
  ret

函数顶部的检查会跳过无效读取(如预期的那样)

有人可能会争辩说,以这种方式使用空引用是一种相当狡猾的做法,我很想同意,即使我不确切知道为什么。但是我在boost :: error_code类中遇到了同样的习惯用法,它使用了boost :: throws():docssource

我知道短路eval不适用于超载||的类型和&amp;&amp;运营商,但显然不是这种情况。

这是UB吗? (用户错误),编译器错误或其他什么?

更新

我对boost :: error_code的原始引用是针对版本1.65.1的。此实现首次引入boost版本1.40。我已经发现在最新版本的增强版中,功能被修改可能是为了避免UB,但很奇怪它被允许长期不受挑战。新函数使用非零整数常量:

namespace detail
{
    //  Misuse of the error_code object is turned into a noisy failure by
    //  poisoning the reference. This particular implementation doesn't
    //  produce warnings or errors from popular compilers, is very  efficient
    //  (as determined by inspecting generated code), and does not suffer
    //  from order of initialization problems. In practice, it also seems
    //  cause user function error handling implementation errors to be detected
    //  very early in the development cycle.
    inline system::error_code* throws()
    {
      // See github.com/boostorg/system/pull/12 by visigoth for why the return
      // is poisoned with (1) rather than (0). A test, test_throws_usage(), has
      // been added to error_code_test.cpp, and as visigoth mentioned it fails
      // on clang for release builds with a return of 0 but works fine with (1).
      return reinterpret_cast<system::error_code*>(1);
    }
}

inline system::error_code& throws() { return *detail::throws(); }

2 个答案:

答案 0 :(得分:3)

这是未定义的行为:

    { return *static_cast<MyObject*>    (nullptr); }

您不能将nullptr转换为引用。它也打破了任何有参考资格的假设。

注意:未定义的行为意味着任何事情都可能发生(包括崩溃或不崩溃)。

答案 1 :(得分:1)

短路不会影响它。您调用了null_obj(),这是一个取消引用空指针的函数。因此,您的程序具有未定义的行为,句点。

此规则不取决于您是否尝试稍后从所述损坏的值中读取某些数据。

如果Boost这样做,那么Boost有一个bug。虽然Boost不太可能这样做。