LLVM 项目博客

LLVM 项目新闻和来自实战的细节

使用 -fsanitize=undefined 测试 libc++

[这篇文章以略微扩展的形式重新发布在Marshall 的博客上]

在我上一篇文章使用 Address Sanitizer 测试 libc++之后,我想:“我还能够运行哪些其他测试呢?”

Address Sanitizer (ASan) 不是 clang 提供的唯一“sanitizer”。还有“Thread Sanitizer” (TSan)、“Undefined Behavior Sanitizer” (UBSan) 等等。在 clang 的 3.3 版本中有一个名为 IOC 的整数溢出 sanitiser。UBSan 的文档可以在LLVM 网站上找到

我一直在查看使用 UBSan 启用的 libc++ 测试套件的结果。即使你对 libc++ 本身不感兴趣,这篇文章也可以作为对 Clang 有用错误检测器的有用介绍,并展示了它们可以发现的几种问题类别。

机制

与 ASan 一样,UBSan 也是一个编译器传递和定制的运行时库。你可以通过向编译器和链接器传递 -fsanitize=undefined 来启用它。我像这样运行 libc++ 测试套件

cd $LLVM/libcxx/test
CC=/path/to/tot/clang OPTIONS="--std=c++11 -stdlib=libc++ -fsanitize=undefined" ./testit

不幸的是,这失败了;使用未发布的编译器和库,我需要 libc++.dylib 和 libc++abi.dylib 的更新版本。因此,我从源代码构建了这些库,然后使用 DYLD_LIBRARY_PATH 来确保测试程序使用我刚构建的库。(我不想替换 /usr/lib 中的那些库,因为系统中的很多东西都依赖于它们)

cd $LLVM/libcxx/test
DYLD_LIBRARY_PATH=$LLVM/libcxx/lib:$LLVM/libcxxabi/lib CC=/path/to/tot/clang OPTIONS="-std=c++11 -stdlib=libc++ -fsanitize=undefined -L $LLVM/libcxxabi/lib -lc++abi" ./testit

其中,像以前一样,“/path/to/tot/clang” 是我刚从源代码构建的 clang,而 $LLVM 是我从 Subversion 中检出 LLVM 各个部分的地方。

结果

测试顺利进行。在上一篇文章中,我注意到这些测试在我的 MacBook Pro 上运行大约需要 30 分钟。ASan 测试大约需要 90 分钟。当 UBSan 测试在大约 42 分钟内完成时,我感到惊喜,这比基线测试慢了大约 40%。在正常情况下,有 12 个测试(超过 4800 个)失败了。使用 UBSan,有 49 个测试失败,UBSan 报告了大约 48463 个不同的运行时错误。

失败的测试

在 UBSan 下失败的 37 个测试中,有 34 个测试因 uncaught exception of type XXXX 而中止,其中 XXX 来自标准库(例如,std::out_of_range)。这是由 libc++ 和 libc++abi 之间的差异造成的,具体来说是由于我自定义构建的 libc++ 和我自定义构建的 libc++abi 都包含了一些标准异常类别的 typeinfo 记录。让这一点正确,并让测试基础设施的所有部分使用正确的库,很快就变成了一个大问题,我仍然没有找到一个好的解决方案。

但是,我能够说服自己,这些失败不是由于 libc++、测试套件或 UBSan 中的错误造成的。

另外三个失败是在 std::thread 测试套件中。当我调查时,发现一些线程测试中存在竞态条件。竞态条件?在 threading 代码中?不可能!

显然,UBSan 下的运行时环境足够不同,以至于在这些测试中触发了(潜在的)竞态条件。查看测试套件,我还发现另外 10 个测试中也存在相同的竞态条件。我提交了修订版 178029,以修复所有 13 个测试中的此问题。

错误消息

48K 个错误!我无法查看 48K 个错误消息;所以我决定将它们归类。

有 37675 条消息的格式为:0x000106ae3fff: runtime error: value inf is outside the range of representable values of type 'xxxx' 其中“xxxx”可以是“double”或“float”(这也包括“-inf”)。

以及 10693 条消息的格式为:0x000101a8f244: runtime error: value nan is outside the range of representable values of type 'xxxx'; 其中“xxxx”可以是“double”或“float”。

有 52 条消息的格式为: what.pass.cpp:24:9: runtime error: member call on address 0x7fff5e8f48d0 which does not point to an object of type 'std::logic_error'.

有 29 条消息如下: eval.pass.cpp:180:14: runtime error: division by zero

有 6 条消息如下:/Sources/LLVM/libcxx/include/memory:3163:25 runtime error: load of misaligned address 0x7fff569a85c6 for type 'const unsigned long', which requires 8 byte alignment

有 5 条消息如下:0x0001037a329e: runtime error: load of value 4294967294, which is not a valid value for type 'std::regex_constants::match_flag_type'

有 2 条消息如下:/Sources/LLVM/libcxx/include/locale:3361:48: runtime error: index 40 out of bounds for type 'char_type [10]'

 有一条消息如下:runtime error: load of value 64, which is not a valid value for type 'bool'

我注意到第一件事是,有时 UBSan 会提供文件和行号,否则只提供十六进制地址。文件和行号对于追踪问题非常有用。

分析

从下往上分析

load of value 64, which is not a valid value for type 'bool' 消息来自其中一个原子测试,它试图清除并设置一个已默认构造的原子标志。我不知道这里正确的行为是什么;我仍在查看这个问题。

index 40 out of bounds for type 'char_type [10]' 错误来自 libc++ 中的货币格式化测试,并且只在测试的“宽字符串”版本上失败;即,使用两个(或四个)字节的字符。有问题的代码行是
*__nc = __src[find(__atoms, __atoms+sizeof(__atoms), *__w) - __atoms];
问题是,sizeof(__atoms) 被假定与该数组中的条目数量相同。对于字符数组来说很好,但对于宽字符数组来说就不好了。在修订版 177694 中修复。

load of value 4294967294, which is not a valid value for type 'std::regex_constants::match_flag_type' 错误事实证明也很容易修复,只要我们确定了正确的修复方法。

这证明很复杂,因为它涉及仔细阅读标准文档。问题是 match_flag_type 是一个枚举,模拟了一个位掩码。该类型也具有一个 operator ~(),它翻转了该类型中的所有位。但由于该类型是用枚举实现的,它有一个底层的整数类型来表示,而 operator ~ 只是翻转了所有位。这会导致 UBSan 不喜欢的值。随后进行了一场广泛的讨论,提出了诸如“这是否重要”和“任何代码都能判断吗”等等观点。最终,我只是将 operator ~ 更改为只翻转枚举中有效的位。在修订版 177693 中修复。

load of misaligned address 0x7fff569a85c6 for type 'const unsigned long', which requires 8 byte alignment 位于字符串的哈希代码中。它们是一种性能优化,我没有尝试修改它们。无论在这里做了什么更改都必须非常小心,因为这会影响所有关联容器的性能。

“除以零”消息出现在三个不同的测试中。数值限制测试中有 3 个,它们是故意存在的。复数测试中有 2 个,它们也是故意存在的。另外 24 个出现在随机数测试套件中,其中测试生成了一堆随机数(使用各种分布),并检查均值、方差、标准差、偏度等是否都符合程序员的预期。问题在于最后一个度量:偏度。它是由某个计算值除以方差得到的。如果方差为零,则偏度应为无穷大。随机数套件中的许多测试都在测试随机数生成器的“边缘情况”,而这些边缘情况中的一些会生成所有数字都相同的序列(因此,方差 == 0)。我们通过注释掉这些退化情况下的偏度计算来解决这个问题,并在测试源文件中留下注释。Howard 在修订版 177826 中修复了这个问题。

runtime error: member call on address 0x7fff5e8f48d0 which does not point to an object of type 'std::logic_error' 消息事实证明是由于 UBSan 中的一个错误造成的。

我刚开始处理 inf/-inf/nan 消息(大约 48K 个)。大多数这些消息来自复数回归测试。由于这是一个用于实现一堆数值例程的库的测试套件,很多测试实际上确实生成和使用 nan/inf,所以我预计很多这些将是“假阳性”。Richard Smith 指出

C++ 标准对 Inf 和 NaN 值的处理高度未定义,因此,在大多数情况下,不清楚什么是定义的行为,什么不是。 
无论如何……我正在更新 UBSan 以抑制将“Inf”和“NaN”在浮点类型之间转换的诊断信息,并且可能会为浮点类型转换中的有限溢出拆分一个单独的标志,以便用户可以根据需要将其关闭。我认为这在目前来说是一个合理的折衷方案。

结论

这项练习虽然还没有完成,但已经发现了一系列 libc++ 测试套件中的错误,以及 libc++ 中的一个错误,以及 libc++ 中的一些未定义行为。这里还有更多内容要查看,但我认为这是一次很好的练习。这里存在一种期望不匹配,特别是在复数和数值测试套件中,因为 UBSan 在寻找 nan/inf/-inf,而 libc++ 测试代码是在故意生成它们。

感谢 Howard Hinnant 对 C++ 标准、libc++ 和 libc++ 测试套件的耐心和解释,以及 Richard Smith 对 UBSan 和解释 C++ 标准的帮助。