valgrind 使用 std::string 报告无效读取

Posted

技术标签:

【中文标题】valgrind 使用 std::string 报告无效读取【英文标题】:valgrind reporting invalid read with std::string 【发布时间】:2018-09-11 06:43:39 【问题描述】:

我正在编写在树莓派 3 上运行的代码。我的日志记录类出现以下错误。

==1297== Invalid read of size 8
==1297==    at 0x4865D1C: ??? (in /usr/lib/arm-linux-gnueabihf/libarmmem.so)
==1297==  Address 0x4c8d45c is 100 bytes inside a block of size 107 alloc'd
==1297==    at 0x4847DA4: operator new(unsigned int) (vg_replace_malloc.c:328)
==1297==    by 0x49C3D9B: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::reserve(unsigned int) (in /usr/lib/arm-linux-gnueabihf/libstdc++.so.6.0.22)
==1297==    by 0x4AE65: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(char const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (basic_string.tcc:1155)
==1297==    by 0xF82B5: Log::Book::addField(std::unique_ptr<Log::Entry, std::default_delete<Log::Entry> >&, unsigned int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (LogBook.cpp:149)
==1297==    by 0xF7CCB: Log::Book::record(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int, unsigned int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::chrono::time_point<std::chrono::_V2::system_clock, std::chrono::duration<long long, std::ratio<1ll, 1000000000ll> > >) (LogBook.cpp:87)

GCC 版本:gcc 版本 6.3.0 20170516 (Raspbian 6.3.0-18+rpi1+deb9u1) valgrind 版本:valgrind-3.13.0 我似乎无法找到问题,因为函数 Log::Book::record() 通过按值传递获取它的值。我也可以说,调用函数时并不总是显示此错误。在错误显示在哪一行和不显示在哪一行的意义上,它是确定性的。任何人都可以指导我这个问题是什么以及解决方案吗?下面的代码 sn-p 带有对指示行的注释。

/** log message */
void Book::record(std::string file, const int line, const unsigned int level, Identifier id, const std::string message,
                  const std::chrono::high_resolution_clock::time_point timeStamp)

    if (!(fileLevels & level) && !(consoleLevels & level))  return; 

    auto now = Time::keeper->now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(timeStamp - Time::globalEpoch);

    //generate message
    auto entry = std::make_unique<Entry>(level);

    // Time since startup
    addField(entry, 0, std::to_string(duration.count()));

    //UTC Time
    addField(entry, 1, now.dateTime());

    // File
    std::string stringFile;
    if (!file.empty())
    
        stringFile = URLfile.lastPathComponent();
    
    addField(entry, 2, stringFile);

    //Line number
    addField(entry, 3, std::to_string(line));

    //ID
    addField(entry, 4, id);

    //Message
    std::string stringMessage;
    if(!message.empty())
    
        addField(entry, 5, message); //this is line LogBook.cpp:87
    
    else
    
        addField(entry, 5, " empty message.");
    
    *entry << ";";

    //queue message
    this->append(std::move(entry));

void Book::addField(std::unique_ptr<Entry> &entry, unsigned int index, const std::string &text)

    std::string textOutput;

    if ((spacings.at(index) != 0) && (text.length() > (spacings.at(index) - 1)))
    
        spacings.at(index) = (uint8_t) (text.length() + 2);
    

    entry->setWidth(spacings.at(index));

    if(entry->empty())
        textOutput = text;
    else
        textOutput = ";" + text;   //This is line LogBook.cpp:149

    if(!textOutput.empty())
        (*entry) << textOutput;

调用此函数并出现此问题的代码。

auto node = child(items, "item", index);
auto enabled = boolValue(node, "enabled", false);
auto file = pathValue(node, key::path);
auto name = stringValue(node, "name", "");
auto type = stringValue(node, "type");

CLOG(CLOG::WARNING, "Yard item " + name + " not enabled, path:" + file.path());

更新 1: 我使用带有选项的 cmake 进行编译。并添加了额外的选项。这些都没有解决问题。

add_compile_options(-ggdb)
add_compile_options(-O1)

#Extra disable vectorization
add_compile_options(-fno-tree-vectorize)
add_compile_options(-fno-tree-loop-vectorize)
add_compile_options(-fno-tree-slp-vectorize)

更新 2: 我发现了另一个使用字符串连接的地方,并且 valgrind 报告了相同的错误


更新 3: 一些时间和有趣的发现。 共享库 libarmmem.so 中发生错误。这会被动态加载,因此总是在不同的地址上。错误发生时使用 gdb 和 valgrind 组合来中断。 gdb 加载了带有起始地址的共享库。

(gdb) info sharedlibrary
From        To          Syms Read   Shared Object Library
0x0483246c  0x04832750  Yes         /usr/local/lib/valgrind/vgpreload_core-arm-linux.so
0x04846e60  0x04850c10  Yes         /usr/local/lib/valgrind/vgpreload_memcheck-arm-linux.so
0x04863588  0x048672fc  Yes (*)     /usr/lib/arm-linux-gnueabihf/libarmmem.so
...

valgrind 报告的错误。

==9442== Invalid read of size 8
==9442==    at 0x4865D34: ??? (in /usr/lib/arm-linux-gnueabi/libarmmem.so)

我们从 libarmmem.so 的 readelf 中得知 .text 部分从 588 开始,而 memcpy 位于 710。此断点的反汇编显示我们位于地址 0x04863710 的 memcpy 中。如果我们检查如下范围:0x04863588 - 0x04863710 = 188. 188 + 588(.text 的起始地址) = 710. 反汇编表明它发生在装配线上。 vldmia 是加载向量浮点寄存器的指令。

0x04865d34 <+9764>: vldmia  r1!, d9

还没有解决办法。

【问题讨论】:

使用-O1 编译并重新运行您的测试。 -O2 及以上会产生虚假的发现。另请参阅The Valgrind Quick Start Guide。 @jww 我已经在使用标志 -ggdb -O0 -Wextra -std=c++14 进行编译。我想 valgrind 可以使用 -ggdb。 遗憾的是,valgrind 无法区分由于操作矢量化而发生的故意越界(“合法”)和意外发生的越界。尝试禁用编译器的矢量化(是的,在 O0 模式下可能会出现新的问题),看看是否仍然发生 根据 valgrind 的输出,它发生在 textOutput = ";" + text; reserve 的调用发生在它创建新的临时字符串对象时,该对象必须适合operator+ 内的串联结果。除非堆以某种方式损坏,否则那里不应该有问题。 该输出并不意味着 Log::Book::addField 或字符串函数内部发生错误。这意味着在 libarmmem.so 内部某处发生了错误,但 valgrind 无法确定调用该库的函数,并且该错误涉及一个指针,该指针位于之前由 std::string 函数在 Log::Book::addField 期间分配的内存末尾附近。 【参考方案1】:

很可能 libarmem.so 中的代码已经过矢量化,它仅在读取完整的 8 字节块后才意识到存在终止字符。这不会触发处理器异常(因为算法会确保指针对齐并因此保持在同一页面中),但会导致 Valgrind 等工具报告误报。

这样的问题会随着时间的推移而变得越来越严重,并且会降低 Valgrind 在实践中的用处。请参阅Valgrind vs Optimising Compilers 进行深入讨论或this bug in diff 了解真实示例(或my Debian suppression list 了解更多示例)。

【讨论】:

以上是关于valgrind 使用 std::string 报告无效读取的主要内容,如果未能解决你的问题,请参考以下文章

用于运行时识别的编译时间字符串分配

android编译环境用mmm编译,报错'string' does not name a type

我啥时候应该使用 std::string / std::string_view 作为参数/返回类型

如何使用参数将 std::string 复制到 std::string?

错误使用 std::vector<std::string> myString

使用 std::string 打开文件