我在 Ubuntu 20.04 LTS 上使用 clang++ 10,带有-fsanitize-undefined-trap-on-error -fsanitize=address,undefined,nullability,implicit-integer-truncation,implicit-integer-arithmetic-value-change,implicit-conversion,integer
我的代码正在生成随机字节
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint8_t> dd(0, 255);
...
ch = uint8_t(dd(gen));
最后一行导致清理器报告未定义的行为,以 bits/random.tcc 为单位
template<...> void mersenne_twister_engine<...>::
_M_gen_rand(void) {
const _UIntType __upper_mask = (~_UIntType()) << __r;
const _UIntType __lower_mask = ~__upper_mask;
for (size_t __k = 0; __k < (__n - __m); ++__k)
{
_UIntType __y = ((_M_x[__k] & __upper_mask)
| (_M_x[__k + 1] & __lower_mask));
_M_x[__k] = (_M_x[__k + __m] ^ (__y >> 1)
^ ((__y & 0x01) ? __a : 0));
}
for (size_t __k = (__n - __m); __k < (__n - 1); ++__k)
{
_UIntType __y = ((_M_x[__k] & __upper_mask)
| (_M_x[__k + 1] & __lower_mask));
_M_x[__k] = (_M_x[__k + (__m - __n)] ^ (__y >> 1) <<<<===== this line
^ ((__y & 0x01) ? __a : 0));
}
_UIntType __y = ((_M_x[__n - 1] & __upper_mask)
| (_M_x[0] & __lower_mask));
_M_x[__n - 1] = (_M_x[__m - 1] ^ (__y >> 1)
^ ((__y & 0x01) ? __a : 0));
_M_p = 0;
}
错误如下:
/usr/include/c++/10/bits/random.tcc:413:33: runtime error: unsigned integer overflow: 397 - 624 cannot be represented in type 'unsigned long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/include/c++/10/bits/random.tcc:413:33 in
/usr/include/c++/10/bits/random.tcc:413:26: runtime error: unsigned integer overflow: 227 + 18446744073709551389 cannot be represented in type 'unsigned long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/include/c++/10/bits/random.tcc:413:26 in
似乎存在一个明显为负数的差__m-__n == 397 - 624
,但操作数都是无符号的。
减去的变量是定义为size_t __n, size_t __m
的模板参数,因此这不是随机边缘情况,而是正在实现的实际模板。
这是 STL 实现中的错误还是我的用法错误?
一个最小的可重现示例:https://godbolt.org/z/vvjWscPnj
>更新:提交给 GCC https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106469 的问题(不是错误) - 作为"不会修复"关闭GCC的团队称为clang的ubsan无符号整数溢出检查不良做法,因为该行为在ISO C++中是明确定义的(作为模包装)。尽管在PRNG中使用模数算法,但在这种特殊情况下并非如此。
然而,在大多数用户空间代码中,无符号溢出几乎总是要捕获的错误,而GCC STL上的这个非错误会阻止用户从这个有用的检查中受益。
尽管另一个答案表明使用模板参数实例化std::uniform_int_distribution
是每个标准的未定义行为uint8_t
但此处的 UBsan 警告与此无关。
UBSan正在标记Mersenne twister本身的实现,但该实现没有任何未定义的行为或错误。
如果你仔细观察,你会发现冒犯性的表达是
_M_x[__k + (__m - __n)]
其中__k
是通过for
循环从(__n - __m)
到(__n - 1)
范围内的值。
这些操作中涉及的所有类型都是无符号std::size_t
的。因此,这些运算都使用模算术,因此即使__m - __n
是负数并且不能在无符号类型中表示,结果
__k + (__m - __n)
将位于0
和__m - 1
之间,因此用它索引数组不是问题。不涉及未定义的行为、未指定的行为或实现定义的行为。
标记此行为的 UBSan 检查不会标记实际的未定义行为。如果有人意识到这一点,完全可以依赖像这样的无符号算术的环绕行为。未签名的溢出检查仅用于标记此类环绕的实例,而不是故意的。您不应该在可能依赖它的其他人的代码上使用它,或者如果您可能依赖它,则不应该在您自己的代码上使用它。
在-fsanitize=address,undefined,nullability,implicit-integer-truncation,implicit-integer-arithmetic-value-change,implicit-conversion,integer
除address
和undefined
之外的所有检查中,UBsan检查不是标记实际的未定义行为,而是在许多情况下可能是无意的条件。由于上述原因,默认的-fsanitize=undefined
清理器标志默认不启用无符号整数溢出检查。有关详细信息,请参阅 https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html。
在std::uniform_int_distribution
中使用uint8_t
的结果是未定义的,因此:
std::uniform_int_distribution<uint8_t> dd(0, 255); // Don't do this!
您可以使用short
、int
、long
、long long
、unsigned short
、unsigned int
、unsigned long
或unsigned long long
中的任何一种。
引用自rand.req.gen
/1.5
在整个子句 [rand] 中,实例化具有名为
IntType
的模板类型参数的模板:
的效果是未定义的,除非相应的模板参数是 cv 非限定的,并且是short
、int
、long
、long long
、unsigned short
、unsigned int
、unsigned long
或unsigned long long
之一。
如果这没有帮助,请跳过-fsanitize=integer
选项,因为
-fsanitize=integer
:检查未定义或可疑的整数行为(例如无符号整数溢出)。启用signed-integer-overflow
。无符号整数溢出没有未定义的行为。使用-fsanitize=undefined
将自动启用有符号整数溢出检查,因此您不必单独启用它。
如果这仍然没有帮助,则可能是clang++
使用的 gcc 库实现中的错误导致了这种情况。您可以尝试使用clang++
的库实现,看看是否有帮助:
clang++ -stdlib=libc++ ...
unsigned
类型在C++中具有明确定义的包装行为。这就是为什么它们被用于PRNG和其他位操作用例的原因之一,这些用例是需要和期望的(并且是算法所必需的),而不是错误。
GCC 开发人员是对的:将所有未签名的包装视为问题是不合理的。打印出它是"未定义的行为"而不是可能的问题更不合理。如果 clang 的 ubsan 一开始就告诉你它在 C++ 中定义得很好,也许是有意的,你就不必用对他们没有用的错误报告来打扰 GCC 开发人员。 或者,您可以在了解问题后将其表述为功能请求。
但你也是对的:在标头中的库函数中,它们成为你自己代码的一部分,这使得当库代码(例如这个 PRNG)与你自己的代码分离时,当它内联到同一个编译单元中时,很难将库代码(例如这个 PRNG)与你自己的代码分开。 ubsan选项是按文件进行的。
libc++ 的 mt19937 实现在必要时禁用 ubsan 检查。 它是作为LLVM的一部分开发的C++标准库的最新实现,主要与clang一起使用。 如果任何标头库要迎合这个将一些有效的C++操作标记为问题的清理器,那就是libc++。 https://godbolt.org/z/aeY5Yn9c6 表明,将-stdlib=libc++
添加到 Godbolt 上的编译选项中可以让您的测试用例干净地运行。 您必须在本地安装它才能实际使用它。
libc++ 将预处理器宏_LIBCPP_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK
定义为__attribute__((__no_sanitize__("unsigned-integer-overflow")))
(如果支持),因此它可以基于每个函数禁用它。 例如,请参阅 libcxx 的<utility>
标头,其中各种函数使用该标签,并在<random>
中mersenne_twister_engine<...>::seed()
。 但有趣的是,它并没有在任何地方使用它,因此您仍然可以获得溢出检查的好处。
或者,您可以围绕随机数生成编写一个包装器函数,并将其放在单独的.cpp
中,而无需sanitize=integer
。在带有-flto
的发布版本中,它仍然可以内联。 或者,如果您不需要高质量的随机性,请使用 libcrandom(3)
;它是单独编译的,而不是内联标头。 Linux的random()
并不可怕,尽管也不是很好。 其他 PRNG 如 xorshift/xoroshiro 既快速又好,但也将使用unsigned
类型并依靠它们的包装进行乘法和/或加/子,除非它们只像 LFSR 那样使用移位和异或。
在 ISO C++ 中,无法仅将某些未签名的操作标记为预期的包装。
至少有一种语言Rust确实解决了这个问题:对于任何整数类型(包括有符号和无符号)的纯+
、-
、*
、/
等,值范围的溢出总是一个错误。 您可以使用 x.wrapping_sub(y) 进行有符号或无符号减法,并具有明确定义的环绕。 同样适用于 add/mul/div/rem/shift/pow。 还有saturating_sub/添加/等,还有overflowing_...返回包装后的结果和一个布尔值,或返回可以是 None 而不是保存整数的类型的 checked_add/sub/etc。 因此,如果您想对整数溢出大惊小怪,Rust 可能是适合您的语言。
(如果LLVM的后端检查未签名溢出部分是由Rust驱动的,并且有人认为有时公开它以供C++使用可能会很有用,我不会感到惊讶。 但是,在编写代码时,预计会出现误报,而不是考虑到该检查器。
GNU C 整数包装溢出扩展
GCC/Clang 和其他理解 C 和 C++ 的 GNU 方言的编译器内置了整数溢出。 这包括signed
和unsigned
包装添加/子/mul。 但仅适用于(未签名)int
/long
/long long
;你必须弄清楚在libstdc++中用于size_t
哪一个。 (例如,在Windows x64上,size_t
必须long long
,但在x86-64系统V上它是long
)
unsigned long wrapping_sub(unsigned long x, unsigned long y)
{
// return x - y; // ISO C++ without working around sanitize=integer
unsigned long res;
bool borrow = __builtin_usubl_overflow(x, y, &res);
return res;
}
Godbolt 上的测试用例表明,__builtin_usubl_overflow
确实安全地执行了1UL, 2UL
的包装减法。 (制作甚至不尝试检测包装的 asm,因为我们已经告诉编译器这一次操作不是错误。 取消注释return x-y;
确实会捕获溢出。
在标准库代码中,将它用于每个未签名的操作会非常麻烦,其中包装不是错误,这就是为什么 libc++ 在每个函数的基础上禁用未签名包装清理器的原因。
由于无符号数学被明确定义为包装,使用这些 GNU C 内置的无符号版本的正常原因是捕获进位/借用输出,以便您知道它们是否包装。 与其使用 clang 的sanitize=integer
,不如在自己的unsigned
操作中使用这些函数,并assert()
布尔结果为 false(无包装溢出)。