【问题标题】:Program crashes when filesystem::path is destroyed当 filesystem::path 被破坏时程序崩溃
【发布时间】:2021-01-02 06:01:41
【问题描述】:

以下程序崩溃:

#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main()
{
    fs::path p1 = "/usr/lib/sendmail.cf";
 
    std::cout << "p1 = " << p1 << '\n';
}

编译:

$ g++ -std=c++17 pathExistsTest.cpp
$ ./a.out 
p1 = "/usr/lib/sendmail.cf"
[1]    35688 segmentation fault (core dumped)  ./a.out

在 Ubuntu 20.04 上测试,编译器为 GCC 8.4.0。

Valgrind,这里是剪切输出:

==30078==    by 0x4AE5034: QAbstractButton::mouseReleaseEvent(QMouseEvent*) (in /usr/lib/x86_64-linux-gnu/libQt5Widgets.so.5.12.8)
==30078==    by 0x4A312B5: QWidget::event(QEvent*) (in /usr/lib/x86_64-linux-gnu/libQt5Widgets.so.5.12.8)
==30078==  Address 0x2b is not stack'd, malloc'd or (recently) free'd
==30078== 
==30078== 
==30078== Process terminating with default action of signal 11 (SIGSEGV)
==30078==  Access not within mapped region at address 0x2B
==30078==    at 0x13AD9B: std::vector<std::filesystem::__cxx11::path::_Cmpt, std::allocator<std::filesystem::__cxx11::path::_Cmpt> >::~vector() (in /home/(me)/src/tomato/build-src-Desktop-Release/TomatoLauncher)

Full Output
我什至不知道为什么调用向量 dtor ?我只创建了一个path 变量,没有vector&lt;path&gt;

【问题讨论】:

  • 看起来您的代码中某处可能有未定义的行为,这在此处体现。它似乎不在显示的代码中。未定义的行为可能发生在离症状发生的地方很远的地方。编辑:也许分享exists 是如何实现的,因为它似乎是这里唯一可能包含错误的东西。
  • 您是否尝试过启用详细警告的构建?没有警告?您是否尝试过使用诸如Valgrind 之类的内存调试器?它没有报告任何问题?
  • 你要展示一个完整的程序,bug在别处
  • ...works 我...
  • 向量来自这里:code.woboq.org/gcc/libstdc++-v3/include/bits/… -- 它是路径组件,存储为_Cmpt 类型的向量,它派生自path

标签: c++ c++17 libstdc++ std-filesystem


【解决方案1】:

TL;DR

您正在使用 GCC 8.4.0 进行编译,因此您需要明确链接到 -lstdc++fs

由于您使用的是 GCC 8.4.0,因此您使用的是 GNU C++ 标准库,也就是 GCC 8.4.0 版本的 libstdc++ 标头。但是您的系统(Ubuntu 20.04)仅包含来自 GCC 9 的 libstdc++.so.6.0.28。如果您没有明确链接到 -lstdc++fs,那么您会不小心使用来自 GCC 9 的 std::filesystem 符号(通过 libstdc++.so)而不是来自 GCC 8(通过 libstdc++fs.a)。

GCC 8 和 GCC 9 有不兼容的 std::filesystem 类型。更具体地说,它们的二进制布局是不同的。这基本上是一个非常隐蔽的 ODR 违规。您的对象是为 GCC 8 布局分配的,但使用 GCC 9 布局构建。当您尝试销毁它时,析构函数使用 GCC 8 布局并崩溃,因为数据不是它所期望的。


有两段代码使用 path 类型的不同、不兼容的布局。

第一段代码来自libstdc++.so.6.0.28:它包含path::_M_split_cmpts() 的定义,通过内联构造函数path::path(string_type&amp;&amp;, format) 调用。由于构造函数是内联的,因此构造函数本身的代码会生成到您的可执行文件中。因此,您的可执行文件包含对 path::_M_split_cmpts 的调用。

第二段代码在您自己的可执行文件中:它为内联(默认)析构函数path::~path() 及其调用的内联函数生成指令;一直到std::filesystem::__cxx11::path::path&lt;char [21], std::filesystem::__cxx11::path&gt;(char const (&amp;) [21], std::filesystem::__cxx11::path::path&gt;(char const (&amp;) [21], std::filesystem::__cxx11::path::format)


我们怎样才能找到这个?

使用调试器: 遍历 ctor 中的可疑函数会发现:

0x5569716498ed <std::filesystem::__cxx11::path::path<char [21], std::filesystem::__cxx11::path>(char const (&) [21], std::filesystem::__cxx11::path::path>(char const (&) [21], std::filesystem::__cxx11::path::format)+112>       callq  0x5569716491e0 <_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@plt>

这是通过 PLT 进行的调用(因此,可能来自共享对象,并且绝对不是内联的)。我们踏入其中:

(gdb) bt
#0  0x00007f102c60f260 in std::filesystem::__cxx11::path::_M_split_cmpts() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x00005569716498ed in std::filesystem::__cxx11::path::path<char [21], std::filesystem::__cxx11::path> (this=0x7ffe1a07ad60, __source=...)
    at /usr/include/c++/8/bits/fs_path.h:185
#2  0x00005569716493fd in main () at blub.cpp:6

所以,我们可以看到它确实来自/lib/x86_64-linux-gnu/libstdc++.so.6,它是/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28 的符号链接。

我们可以看到的 dtor,例如在 OP 的 Valgrind 输出中:

==30078== Invalid read of size 8
==30078==    at 0x13AD9B: std::vector<std::filesystem::__cxx11::path::_Cmpt, std::allocator<std::filesystem::__cxx11::path::_Cmpt> >::~vector() (in /home/(me)/src/tomato/build-src-Desktop-Release/TomatoLauncher)

它是内联的,因此在可执行文件中。


现在,真正有趣的部分是包含 path 的内联函数和 path::_M_split_cmpts 函数的标头都来自 GNU C++ 标准库 (libstdc++)。

它们怎么会不兼容?

要回答这个问题,让我们看一下确切的版本。我们正在使用 GCC 8.4.0 进行编译。它包含包含路径,它们引用 Ubuntu 20.04 的 gcc-8 包中附带的标准库头文件。这些完全匹配,您必须更改默认设置以使 GCC 使用不同的、不匹配的标准库头文件。因此,标头是 GCC 8.4.0 的标头。

共享对象libstdc++.so呢?根据ldd 和调试器,我们正在使用libstdc++.so.6.0.28 运行。根据libstdc++ ABI Policy and Guidelines,这是 GCC >= 9.3

libstdc++.so.6.0.28 确实包含_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv 的定义:

$ objdump -T /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28 | grep _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
000000000016a260 g    DF .text  00000000000005f3  GLIBCXX_3.4.26 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv

根据 ABI 文档,这是

GCC 9.1.0:GLIBCXX_3.4.26、CXXABI_1.3.12

所以这是一个在 GCC 8.4.0 中不可用的符号。


为什么编译器/链接器不抱怨?

当我们使用 gcc-8 编译时,为什么编译器或链接器不抱怨我们使用了 GCC 9 的符号?

如果我们使用-v 编译,我们会看到链接器调用:

COLLECT_GCC_OPTIONS='-v' '-std=c++17' '-g' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/8/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/8/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/8/lto-wrapper -plugin-opt=-fresolution=/tmp/cceJgWPt.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/8/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/8 -L/usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/8/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/8/../../.. /tmp/ccTNph3u.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/8/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/crtn.o                           COLLECT_GCC_OPTIONS='-v' '-std=c++17' '-g' '-shared-libgcc' '-mtune=generic' '-march=x86-64'

在那里,我们有-L/usr/lib/gcc/x86_64-linux-gnu/8 和其他路径来查找标准库。在那里,我们找到了libstdc++.so -&gt; ../../../x86_64-linux-gnu/libstdc++.so.6,它最终指向了/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28(!!!)。

因此,链接器获得了 GCC 9 的 libstdc++.so,并且它不会从编译器 (*) 接收任何有关符号的版本信息。编译器只知道源代码,而源代码在这种情况下不包含符号版本(GCC 8.4.0 的文件系统头文件)。然而,符号版本存在于 ELF 二进制文件libstdc++.so 中。对于编译器_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv 请求的符号,链接器看到GLIBCXX_3.4.26 并对此感到满意。让您想知道是否有一个链接器开关来告诉链接器“如果我请求未版本化符号,请不要使用版本化符号”。

(*) 链接器没有从编译器接收到关于该未解析符号的任何符号信息,因为编译器没有来自源代码的此类信息。你can add info to your source code。我不知道 libstdc++ 通常是如何做到的——或者它对头文件中符号版本的政策。 filesystem 好像根本没做完。

ELF 符号版本控制机制通常应该防止这种不兼容:如果存在与布局不兼容的更改,则创建一个具有相同名称但版本不同的新符号,并将其添加到 @987654361 @,然后包含旧版本和新版本。

针对libstdc++.so 编译的二进制文件指定了它想要的符号版本,动态加载器会根据名称和版本匹配的符号正确解析未定义的符号。请注意,动态链接器不知道要搜索哪个共享库(在 Windows/PE 上,这是不同的)。任何“符号请求”都只是一个未定义的符号,并且有一个完全独立的所需库列表,这些库将提供那些未定义的符号。但是二进制文件中没有映射哪个符号应该来自哪个库。

因为 ELF 符号版本控制机制允许向后兼容添加符号,所以我们可以为 多个 版本的编译器维护一个单个 libstdc++.so。这就是为什么您会在各处看到符号链接,将所有符号链接到同一个文件。后缀.6.0.28 是另一种正交版本控制方案,它允许向后-不兼容 更改:您的二进制文件可以指定它需要libstdc++.so.6,您可以为其他二进制文件添加不兼容的libstdc++.so.7

有趣的事实:如果你将你的库链接到一个纯 GCC 8 版本的libstdc++.so,你会看到a linker error。链接到共享库对二进制文件没有多大作用。但是,它确实修复了未解析符号的符号版本,并且可以在查看所有库后检查是否没有留下未解析的符号。我们可以看到,当您将二进制文件链接到 libstdc++.so.6.0.28 时,它实际上请求了 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26

有趣的事实 2:如果您针对纯 GCC 8 版本的 libstdc++.so 运行您的库,您会收到动态链接器错误,因为它找不到 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26


实际应该发生什么?

您实际上应该链接到libstdc++fs.a。它还提供了_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv 的定义,它不是符号链接,而是特定于这个 GCC 版本:/usr/lib/gcc/x86_64-linux-gnu/8/libstdc++fs.a

当您链接到-lstdc++fs 时,您会将其符号直接包含在可执行文件中(因为它是一个静态库)。可执行文件中的符号优先于共享对象中的符号。因此,使用来自libstdc++fs.a_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv


path 中的布局实际上不兼容是什么?

GCC 9 引入了一种不同的类型来保存路径的组件。使用clang++ -cc1 -fdump-record-layouts,我们可以在左侧看到偏移量,在右侧看到成员和类型名称:

GCC 8.4.0:

 0 | class std::filesystem::__cxx11::path
 0 |   class std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> > _M_pathname
 0 |     struct std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::_Alloc_hider _M_dataplus
 0 |       class std::allocator<char> (base) (empty)
 0 |         class __gnu_cxx::new_allocator<char> (base) (empty)
 0 |       std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::pointer _M_p
 8 |     std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::size_type _M_string_length
16 |     union std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::(anonymous at /usr/include/c++/8/bits/basic_string.h:160:7) 
16 |       char [16] _M_local_buf
16 |       std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::size_type _M_allocated_capacity
32 |   class std::vector<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> > _M_cmpts
32 |     struct std::_Vector_base<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> > (base)
32 |       struct std::_Vector_base<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> >::_Vector_impl _M_impl
32 |         class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> (base) (empty)
32 |           class __gnu_cxx::new_allocator<struct std::filesystem::__cxx11::path::_Cmpt> (base) (empty)
32 |         std::_Vector_base<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> >::pointer _M_start
40 |         std::_Vector_base<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> >::pointer _M_finish
48 |         std::_Vector_base<struct std::filesystem::__cxx11::path::_Cmpt, class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt> >::pointer _M_end_of_storage
56 |   enum std::filesystem::__cxx11::path::_Type _M_type
   | [sizeof=64, dsize=57, align=8,
   |  nvsize=57, nvalign=8]

GCC 9.3.0:

 0 | class std::filesystem::__cxx11::path
 0 |   class std::__cxx11::basic_string<char> _M_pathname
 0 |     struct std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::_Alloc_hider _M_dataplus
 0 |       class std::allocator<char> (base) (empty)
 0 |         class __gnu_cxx::new_allocator<char> (base) (empty)
 0 |       std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::pointer _M_p
 8 |     std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::size_type _M_string_length
16 |     union std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::(anonymous at /usr/include/c++/9/bits/basic_string.h:171:7) 
16 |       char [16] _M_local_buf
16 |       std::__cxx11::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >::size_type _M_allocated_capacity
32 |   struct std::filesystem::__cxx11::path::_List _M_cmpts
32 |     class std::unique_ptr<struct std::filesystem::__cxx11::path::_List::_Impl, struct std::filesystem::__cxx11::path::_List::_Impl_deleter> _M_impl
32 |       class std::__uniq_ptr_impl<struct std::filesystem::__cxx11::path::_List::_Impl, struct std::filesystem::__cxx11::path::_List::_Impl_deleter> _M_t
32 |         class std::tuple<struct std::filesystem::__cxx11::path::_List::_Impl *, struct std::filesystem::__cxx11::path::_List::_Impl_deleter> _M_t
32 |           struct std::_Tuple_impl<0, struct std::filesystem::__cxx11::path::_List::_Impl *, struct std::filesystem::__cxx11::path::_List::_Impl_deleter> (base)
32 |             struct std::_Tuple_impl<1, struct std::filesystem::__cxx11::path::_List::_Impl_deleter> (base) (empty)
32 |               struct std::_Head_base<1, struct std::filesystem::__cxx11::path::_List::_Impl_deleter, true> (base) (empty)
32 |                 struct std::filesystem::__cxx11::path::_List::_Impl_deleter (base) (empty)
32 |             struct std::_Head_base<0, struct std::filesystem::__cxx11::path::_List::_Impl *, false> (base)
32 |               struct std::filesystem::__cxx11::path::_List::_Impl * _M_head_impl
   | [sizeof=40, dsize=40, align=8,
   |  nvsize=40, nvalign=8]

区别在于path::_M_cmpts

// GCC 8
class std::vector<
  struct std::filesystem::__cxx11::path::_Cmpt,
  class std::allocator<struct std::filesystem::__cxx11::path::_Cmpt>
> _M_cmpts

// GCC 9
struct std::filesystem::__cxx11::path::_List _M_cmpts

您还可以在上面的记录转储中看到path::_List 的结构。它与 GCC 8 vector 非常不兼容。

请记住,我们是通过 GCC 9 中的 libstdc++.so 调用 path::_M_split_cmpts,而我们在 vector 析构函数中崩溃了这个 _M_cmpts 数据成员。

这是从vector 更改为_List 的提交:

commit 4f87bb8d6e8dec21a07f1fba641a78a127281349
Author: Jonathan Wakely <jwakely@redhat.com>
Date:   Thu Dec 13 20:33:55 2018 +0000

PR libstdc++/71044 optimize std::filesystem::path construction

This new implementation has a smaller footprint than the previous
implementation, due to replacing std::vector<_Cmpt> with a custom pimpl
type that only needs a single pointer. The _M_type enumeration is also
combined with the pimpl type, by using a tagged pointer, reducing
sizeof(path) further still.

Construction and modification of paths is now done more efficiently, by
splitting the input into a stack-based buffer of string_view objects
instead of a dynamically-allocated vector containing strings. Once the
final size is known only a single allocation is needed to reserve space
for it.  The append and concat operations no longer require constructing
temporary path objects, nor re-parsing the entire native pathname.
This results in algorithmic improvements to path construction, and
working with large paths is much faster.

【讨论】:

  • 我已经很久没有这么开心了! xD
  • 感谢您的努力!很高兴我的问题能给你带来一些乐趣。 :D
  • 现在我想了想,函数可能在标题中被标记为 ELF 不可见。这将强制从 static 库中获取它们。
猜你喜欢
  • 2023-03-29
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-16
  • 1970-01-01
  • 2018-12-28
  • 1970-01-01
相关资源
最近更新 更多