当 filesystem::path 被销毁时程序崩溃
Program crashes when filesystem::path is destroyed
以下程序崩溃:
#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
我什至不知道为什么要调用 vector dtor?我只创建了一个path
变量,没有vector<path>
.
TL;DR
您正在使用 GCC 8.4.0 进行编译,因此您需要 link 明确反对 -lstdc++fs
。
由于您使用的是 GCC 8.4.0,因此您使用的是 GCC 8.4.0 版本的 GNU C++ 标准库 aka libstdc++ headers。但是你的系统 (Ubuntu 20.04) 只包含来自 GCC 9 的 libstdc++.so.6.0.28
。如果你没有明确地 link 反对 -lstdc++fs
,那么你不小心消耗了 std::filesystem
符号来自 GCC 9(通过 libstdc++.so
)而不是来自 GCC 8(通过 libstdc++fs.a
)。
GCC 8 和 GCC 9 具有不兼容的 std::filesystem
类型。更具体地说,它们的二进制布局不同。这基本上是一个很隐蔽的ODR-violation。您的 object 分配给 GCC 8 布局,但使用 GCC 9 布局构建。当您随后尝试销毁它时,析构函数使用 GCC 8 布局并崩溃,因为数据不是它所期望的。
有两段代码使用不同的、不兼容的 path
类型布局。
第一段代码来自libstdc++.so.6.0.28
:它包含path::_M_split_cmpts()
的定义,通过内联构造函数path::path(string_type&&, format)
调用。由于构造函数是内联的,因此构造函数本身的代码会生成到您的可执行文件中。因此,您的可执行文件包含对 path::_M_split_cmpts
.
的调用
第二段代码在您自己的可执行文件中:它为内联(默认)析构函数 path::~path()
及其调用的内联函数生成指令;一直到 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)
.
我们怎样才能找到这个?
使用调试器:
单步执行 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 的调用(因此,可能来自共享 object,绝对不是内联的)。我们步入其中并:
(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
,它是link到/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
.
的一个symlink
我们可以看到的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
的内联函数的 header 和 path::_M_split_cmpts
函数都来自 GNU C++ 标准库 (libstdc++)。
怎么会不兼容呢?
为了回答这个问题,让我们来看看确切的版本。我们正在使用 GCC 8.4.0 进行编译。它包含路径,它们引用 Ubuntu 20.04 的 gcc-8 包中提供的标准库 headers。它们完美匹配,您必须更改默认设置以使 GCC 使用不同的、不匹配的标准库 headers。 header 因此是 GCC 8.4.0 的
共享的objectlibstdc++.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 中不可用的符号。
为什么compiler/linker不抱怨?
当我们使用 gcc-8 编译时,为什么编译器或 linker 不抱怨我们使用了 GCC 9 中的符号?
如果我们用 -v
编译,我们会看到 linker 调用:
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 -> ../../../x86_64-linux-gnu/libstdc++.so.6
,它最终指向 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
(!!!).
所以 linker 被赋予了 GCC 9 的 libstdc++.so
,并且它没有从编译器 (*) 接收到关于符号的任何版本信息。编译器只知道源代码,在这种情况下,源代码不包含符号版本(GCC 8.4.0 的文件系统headers)。然而,符号版本存在于 ELF 二进制 libstdc++.so
中。 linker 看到 GLIBCXX_3.4.26
编译器请求的符号 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
并且对此感到满意。让您想知道是否有一个 linker 开关告诉 linker “如果我请求了一个未版本化的符号,请不要使用版本化符号”。
(*) linker 没有从编译器接收到关于未解析符号的任何符号信息,因为编译器没有来自源代码的此类信息。你can add info to your source code。我不知道 libstdc++ 通常是如何做到的 - 或者它对 header 文件中符号版本的政策。 filesystem
.
似乎根本没有完成
ELF 符号版本控制机制通常应该防止这种不兼容:如果有 layout-incompatible 更改,您创建一个名称相同但版本不同的新符号,然后 添加 它到 libstdc++.so
,然后包含旧版本和新版本。
针对 libstdc++.so
编译的二进制文件指定它需要哪个版本的符号,动态加载程序根据匹配名称和版本的符号正确解析未定义的符号。请注意,动态 linker 不知道要搜索哪个共享库(在 Windows/PE 上,这是不同的)。任何“符号请求”都只是一个未定义的符号,并且有一个完全独立的所需库列表,这些库应提供那些未定义的符号。但是二进制文件中没有映射哪个符号应该来自哪个库。
因为 ELF 符号版本控制机制允许 backwards-compatible 添加符号,我们可以维护 单个 libstdc++.so
for 多个 编译器的版本。这就是为什么到处都是 symlinks,它们都指向同一个文件。后缀 .6.0.28
是另一种正交版本控制方案,它允许向后 不兼容 更改:您的二进制文件可以指定它需要 libstdc++.so.6
并且您可以添加不兼容的 libstdc++.so.7
对于其他二进制文件。
有趣的事实:如果您link针对 libstdc++.so
的纯 GCC 8 版本编辑您的库,您会看到 a linker error .链接共享库对二进制文件没有多大作用;但是,它确实修复了未解析符号的符号版本,并且可以在查看所有库后检查是否没有留下未解析的符号。我们可以看到,当您 link 它针对 libstdc++.so.6.0.28
.
时,您的二进制文件实际上请求 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26
有趣的事实 2:如果您 运行 您的库针对 libstdc++.so
的纯 GCC 8 版本,您将收到动态 link错误,因为找不到 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26
.
实际应该发生什么?
你实际上应该 link 到 libstdc++fs.a
。它还提供了 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
的定义,它不是 symlink 但特定于此 GCC 版本:/usr/lib/gcc/x86_64-linux-gnu/8/libstdc++fs.a
.
当您 link 反对 -lstdc++fs
时,您会将其符号直接包含到可执行文件中(因为它是静态库)。可执行文件中的符号优先于共享 object 中的符号。因此,使用 libstdc++fs.a
中的 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
。
path
中的布局不兼容到底是什么?
GCC 9 引入了一种不同的类型来保存路径的组件。使用clang++ -cc1 -fdump-record-layouts
,我们可以看到左边是偏移量,右边是成员和类型名称:
海湾合作委员会 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]
海湾合作委员会 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.
以下程序崩溃:
#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
我什至不知道为什么要调用 vector dtor?我只创建了一个path
变量,没有vector<path>
.
TL;DR
您正在使用 GCC 8.4.0 进行编译,因此您需要 link 明确反对 -lstdc++fs
。
由于您使用的是 GCC 8.4.0,因此您使用的是 GCC 8.4.0 版本的 GNU C++ 标准库 aka libstdc++ headers。但是你的系统 (Ubuntu 20.04) 只包含来自 GCC 9 的 libstdc++.so.6.0.28
。如果你没有明确地 link 反对 -lstdc++fs
,那么你不小心消耗了 std::filesystem
符号来自 GCC 9(通过 libstdc++.so
)而不是来自 GCC 8(通过 libstdc++fs.a
)。
GCC 8 和 GCC 9 具有不兼容的 std::filesystem
类型。更具体地说,它们的二进制布局不同。这基本上是一个很隐蔽的ODR-violation。您的 object 分配给 GCC 8 布局,但使用 GCC 9 布局构建。当您随后尝试销毁它时,析构函数使用 GCC 8 布局并崩溃,因为数据不是它所期望的。
有两段代码使用不同的、不兼容的 path
类型布局。
第一段代码来自libstdc++.so.6.0.28
:它包含path::_M_split_cmpts()
的定义,通过内联构造函数path::path(string_type&&, format)
调用。由于构造函数是内联的,因此构造函数本身的代码会生成到您的可执行文件中。因此,您的可执行文件包含对 path::_M_split_cmpts
.
第二段代码在您自己的可执行文件中:它为内联(默认)析构函数 path::~path()
及其调用的内联函数生成指令;一直到 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)
.
我们怎样才能找到这个?
使用调试器: 单步执行 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 的调用(因此,可能来自共享 object,绝对不是内联的)。我们步入其中并:
(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
,它是link到/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
的内联函数的 header 和 path::_M_split_cmpts
函数都来自 GNU C++ 标准库 (libstdc++)。
怎么会不兼容呢?
为了回答这个问题,让我们来看看确切的版本。我们正在使用 GCC 8.4.0 进行编译。它包含路径,它们引用 Ubuntu 20.04 的 gcc-8 包中提供的标准库 headers。它们完美匹配,您必须更改默认设置以使 GCC 使用不同的、不匹配的标准库 headers。 header 因此是 GCC 8.4.0 的
共享的objectlibstdc++.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 中不可用的符号。
为什么compiler/linker不抱怨?
当我们使用 gcc-8 编译时,为什么编译器或 linker 不抱怨我们使用了 GCC 9 中的符号?
如果我们用 -v
编译,我们会看到 linker 调用:
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 -> ../../../x86_64-linux-gnu/libstdc++.so.6
,它最终指向 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
(!!!).
所以 linker 被赋予了 GCC 9 的 libstdc++.so
,并且它没有从编译器 (*) 接收到关于符号的任何版本信息。编译器只知道源代码,在这种情况下,源代码不包含符号版本(GCC 8.4.0 的文件系统headers)。然而,符号版本存在于 ELF 二进制 libstdc++.so
中。 linker 看到 GLIBCXX_3.4.26
编译器请求的符号 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
并且对此感到满意。让您想知道是否有一个 linker 开关告诉 linker “如果我请求了一个未版本化的符号,请不要使用版本化符号”。
(*) linker 没有从编译器接收到关于未解析符号的任何符号信息,因为编译器没有来自源代码的此类信息。你can add info to your source code。我不知道 libstdc++ 通常是如何做到的 - 或者它对 header 文件中符号版本的政策。 filesystem
.
ELF 符号版本控制机制通常应该防止这种不兼容:如果有 layout-incompatible 更改,您创建一个名称相同但版本不同的新符号,然后 添加 它到 libstdc++.so
,然后包含旧版本和新版本。
针对 libstdc++.so
编译的二进制文件指定它需要哪个版本的符号,动态加载程序根据匹配名称和版本的符号正确解析未定义的符号。请注意,动态 linker 不知道要搜索哪个共享库(在 Windows/PE 上,这是不同的)。任何“符号请求”都只是一个未定义的符号,并且有一个完全独立的所需库列表,这些库应提供那些未定义的符号。但是二进制文件中没有映射哪个符号应该来自哪个库。
因为 ELF 符号版本控制机制允许 backwards-compatible 添加符号,我们可以维护 单个 libstdc++.so
for 多个 编译器的版本。这就是为什么到处都是 symlinks,它们都指向同一个文件。后缀 .6.0.28
是另一种正交版本控制方案,它允许向后 不兼容 更改:您的二进制文件可以指定它需要 libstdc++.so.6
并且您可以添加不兼容的 libstdc++.so.7
对于其他二进制文件。
有趣的事实:如果您link针对 libstdc++.so
的纯 GCC 8 版本编辑您的库,您会看到 a linker error .链接共享库对二进制文件没有多大作用;但是,它确实修复了未解析符号的符号版本,并且可以在查看所有库后检查是否没有留下未解析的符号。我们可以看到,当您 link 它针对 libstdc++.so.6.0.28
.
_ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26
有趣的事实 2:如果您 运行 您的库针对 libstdc++.so
的纯 GCC 8 版本,您将收到动态 link错误,因为找不到 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv@GLIBCXX_3.4.26
.
实际应该发生什么?
你实际上应该 link 到 libstdc++fs.a
。它还提供了 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
的定义,它不是 symlink 但特定于此 GCC 版本:/usr/lib/gcc/x86_64-linux-gnu/8/libstdc++fs.a
.
当您 link 反对 -lstdc++fs
时,您会将其符号直接包含到可执行文件中(因为它是静态库)。可执行文件中的符号优先于共享 object 中的符号。因此,使用 libstdc++fs.a
中的 _ZNSt10filesystem7__cxx114path14_M_split_cmptsEv
。
path
中的布局不兼容到底是什么?
GCC 9 引入了一种不同的类型来保存路径的组件。使用clang++ -cc1 -fdump-record-layouts
,我们可以看到左边是偏移量,右边是成员和类型名称:
海湾合作委员会 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]
海湾合作委员会 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.