使用 libpthread 的共享库中的未定义行为,但在 ELF 中没有将其作为依赖项
undefined behavior in shared lib using libpthread, but not having it in ELF as dependency
当链接 "properly"(进一步解释)时,下面的两个函数调用无限期地阻塞在实现 cv.notify_one
和 cv.wait_for
:
的 pthread 调用上
// let's call it odr.cpp, which forms libodr.so
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void Notify() {
std::chrono::milliseconds(100);
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
void Get() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::milliseconds(300));
}
在以下应用程序中使用上述共享库时:
// let's call it test.cpp, which forms a.out
int main() {
std::thread thr([&]() {
std::cout << "Notify\n";
Notify();
});
std::cout << "Before Get\n";
Get();
std::cout << "After Get\n";
thr.join();
}
问题仅在链接时重现 libodr.so
:
- 使用 g++
- 带黄金链接器
- 提供
-lpthread
作为依赖项
使用以下相关工具版本:
Linux Mint 18.3 Sylvia
binutils 2.26.1-1ubuntu1~16.04.6
g++ 4:5.3.1-1ubuntu1
libc6:amd64 2.23-0ubuntu10
所以我们最终得到:
__pthread_key_create
在PLT 中定义为弱符号
- 没有
libpthread.so
作为 ELF 中的依赖项
如图所示:
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
10: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create
另一方面,我们没有遇到以下任何错误:
- 叮当++
- bfd 链接器
- 没有明确的
-lpthread
-lpthread
但 -Wl,--no-as-needed
注意:这次我们有:
NOTYPE
并且没有 libpthread.so
依赖关系
WEAK
和 libpthread.so
依赖关系
如图所示:
$ clang++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
24: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create@GLIBC_2.2.5 (7)
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=bfd -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
14: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __pthread_key_create
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
18: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __pthread_key_create
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -Wl,--no-as-needed -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
10: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create@GLIBC_2.2.5 (4)
compile/run 的完整示例可在此处找到:https://github.com/aurzenligl/study/tree/master/cpp-pthread
当 __pthread_key_create
是 WEAK
并且找不到 ELF 中的 libpthread.so
依赖项时,什么会破坏使用 pthread 的 shlib?动态链接器是否从 libc.so
(存根)而不是 libpthread.so
中获取 pthread 符号?
这里发生了很多事情:gcc 和 clang 之间的差异,gnu ld 和 gold 之间的差异,--as-needed
linker 标志,两种不同的故障模式,甚至可能是一些时间问题。
让我们从如何 link 使用 POSIX 线程的程序开始。
编译器的 -pthread
标志就是您所需要的。这是一个编译器标志,因此在编译使用线程的代码和 link 最终执行 table 时都应该使用它。当您在 link 步骤中使用 -pthread
时,编译器将自动提供 -lpthread
标志,并在 link 行的正确位置。
通常,您只会在 link 最终执行 table 时使用它,而不会在 link 共享库时使用它。如果你只是想让你的库线程安全,但不想强制每个使用你的库的程序都使用 pthreads link,你会想要使用运行时检查来查看是否加载了 pthreads 库, 并且仅当它是时才调用 pthread API。在 Linux 上,这通常是通过检查 "canary" 来完成的——例如,对像 __pthread_key_create
这样的任意符号进行弱引用,只有在加载库时才会定义它,如果程序 linked 没有它,则值为 0。
但是,在您的情况下,您的库 libodr.so
很大程度上取决于线程,因此使用 -pthread
标志 link 它是合理的。
这给我们带来了第一个失败模式:如果您在两个 link 步骤中都使用 g++ 和 gold,程序会抛出 std::system_error
并提示您需要启用多线程。这是由于 --as-needed
标志。 GCC 默认将 --as-needed
传递给 linker,而 clang(显然)不会。使用 --as-needed
,linker 将只记录解析强引用的库依赖项。由于对 pthread API 的所有引用都很弱,其中 none 足以告诉 linker libpthread.so 应该添加到依赖列表(通过 DT_NEEDED
条目在动态 table)。改成clang或者加一个-Wl,--no-as-needed
标志就解决了这个问题,程序会加载pthread库
但是,等等,为什么在使用 Gnu linker 时不需要这样做?它使用相同的规则:只有强引用才会导致库被记录为依赖项。不同之处在于 Gnu ld 还考虑来自其他共享库的引用,而 gold 只考虑来自常规目标文件的引用。事实证明,pthread 库提供了几个 libc 符号的覆盖定义,并且 libstdc++.so
对其中一些符号(例如 write
)有强引用。这些强引用足以让 Gnu ld 将 libpthread.so
记录为依赖项。这与其说是设计,不如说是意外。我不认为更改 gold 以考虑来自其他共享库的引用实际上是一个可靠的修复。我认为正确的解决方案是 GCC 在使用 -pthread
.
时将 --no-as-needed
放在 -lpthread
标志前面
这引出了一个问题,即为什么在使用 POSIX 线程和黄金 linker 时这个问题不会一直出现。但这是一个小测试程序;一个更大的程序几乎肯定会包含对 libpthread.so
覆盖的某些 libc 符号的强引用。
现在让我们看一下第二种失败模式,如果您 link libodr.so
使用 g++、gold 和 [=13=,Notify()
和 Get()
都会无限期地阻塞].
在 Notify()
中,您在调用 cv.notify_one()
时一直持有锁直到函数结束。你真的只需要持有锁来设置就绪标志;如果我们更改它以便在此之前释放锁,那么调用 Get()
的线程将在 300 毫秒后超时,并且不会阻塞。所以真正阻塞的是对 notify_one()
的调用,并且程序处于死锁状态,因为 Get()
正在等待同一个锁。
那为什么只有当__pthread_key_create
是FUNC
而不是NOTYPE
时才阻塞呢?我认为符号的类型是一个转移注意力的问题,真正的问题是 gold 没有记录由未添加为所需库的库解析的引用的符号版本。 wait_for
的实现调用了 pthread_cond_timedwait
,它在 libpthread
和 libc
中都有两个版本。加载器可能将引用绑定到错误的版本,导致无法解锁互斥体而导致死锁。我为 gold 做了一个临时补丁来记录这些版本,这让程序运行起来了。不幸的是,这不是解决方案,因为该补丁可能会导致 ld.so 在其他情况下崩溃。
我尝试将cv.wait_for(...)
更改为cv.wait(lock, []{ return ready; })
,程序在所有情况下都运行完美,这进一步表明问题出在pthread_cond_timedwait
。
底线是添加 --no-as-needed
标志将解决这个非常小的测试用例的问题。任何更大的东西都可能在没有额外标志的情况下工作,因为您将增加对 libpthread
中的符号进行强引用的可能性。 (例如,在 odr.cpp
中添加对 std::this_thread::sleep_for
anywhere 的调用会添加对 nanosleep
的强引用,从而将 libpthread
放入需要的清单。)
更新: 我已经确认失败的程序 link 正在访问 pthread_cond_timedwait
的错误版本。对于 glibc 2.3.2,pthread_cond_t
类型已更改,使用该类型的旧版本 API 已更改为动态分配新的(更大的)结构并在原始类型中存储指向它的指针。所以现在,如果消费线程在生产线程达到 cv.notify_one
之前达到 cv.wait_for
,cv.wait_for
的实现将调用旧版本的 pthread_cond_timedwait
,它会初始化它认为是cv
中的旧 pthread_cond_t
带有指向新 pthread_cond_t
的指针。之后,当另一个线程到达 cv.notify_one
时,它的实现假定 cv
包含一个新样式 pthread_cond_t
而不是指向一个的指针,因此它调用 pthread_mutex_lock
指向新 pthread_cond_t
的指针而不是指向互斥体的指针。它锁定了那个可能的互斥锁,但它永远不会被解锁,因为另一个线程解锁了真正的互斥锁。
当链接 "properly"(进一步解释)时,下面的两个函数调用无限期地阻塞在实现 cv.notify_one
和 cv.wait_for
:
// let's call it odr.cpp, which forms libodr.so
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void Notify() {
std::chrono::milliseconds(100);
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
void Get() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::milliseconds(300));
}
在以下应用程序中使用上述共享库时:
// let's call it test.cpp, which forms a.out
int main() {
std::thread thr([&]() {
std::cout << "Notify\n";
Notify();
});
std::cout << "Before Get\n";
Get();
std::cout << "After Get\n";
thr.join();
}
问题仅在链接时重现 libodr.so
:
- 使用 g++
- 带黄金链接器
- 提供
-lpthread
作为依赖项
使用以下相关工具版本:
Linux Mint 18.3 Sylvia
binutils 2.26.1-1ubuntu1~16.04.6
g++ 4:5.3.1-1ubuntu1
libc6:amd64 2.23-0ubuntu10
所以我们最终得到:
__pthread_key_create
在PLT 中定义为弱符号
- 没有
libpthread.so
作为 ELF 中的依赖项
如图所示:
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
10: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create
另一方面,我们没有遇到以下任何错误:
- 叮当++
- bfd 链接器
- 没有明确的
-lpthread
-lpthread
但-Wl,--no-as-needed
注意:这次我们有:
NOTYPE
并且没有libpthread.so
依赖关系WEAK
和libpthread.so
依赖关系
如图所示:
$ clang++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
24: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create@GLIBC_2.2.5 (7)
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=bfd -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
14: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __pthread_key_create
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
18: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __pthread_key_create
$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -Wl,--no-as-needed -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
10: 0000000000000000 0 FUNC WEAK DEFAULT UND __pthread_key_create@GLIBC_2.2.5 (4)
compile/run 的完整示例可在此处找到:https://github.com/aurzenligl/study/tree/master/cpp-pthread
当 __pthread_key_create
是 WEAK
并且找不到 ELF 中的 libpthread.so
依赖项时,什么会破坏使用 pthread 的 shlib?动态链接器是否从 libc.so
(存根)而不是 libpthread.so
中获取 pthread 符号?
这里发生了很多事情:gcc 和 clang 之间的差异,gnu ld 和 gold 之间的差异,--as-needed
linker 标志,两种不同的故障模式,甚至可能是一些时间问题。
让我们从如何 link 使用 POSIX 线程的程序开始。
编译器的 -pthread
标志就是您所需要的。这是一个编译器标志,因此在编译使用线程的代码和 link 最终执行 table 时都应该使用它。当您在 link 步骤中使用 -pthread
时,编译器将自动提供 -lpthread
标志,并在 link 行的正确位置。
通常,您只会在 link 最终执行 table 时使用它,而不会在 link 共享库时使用它。如果你只是想让你的库线程安全,但不想强制每个使用你的库的程序都使用 pthreads link,你会想要使用运行时检查来查看是否加载了 pthreads 库, 并且仅当它是时才调用 pthread API。在 Linux 上,这通常是通过检查 "canary" 来完成的——例如,对像 __pthread_key_create
这样的任意符号进行弱引用,只有在加载库时才会定义它,如果程序 linked 没有它,则值为 0。
但是,在您的情况下,您的库 libodr.so
很大程度上取决于线程,因此使用 -pthread
标志 link 它是合理的。
这给我们带来了第一个失败模式:如果您在两个 link 步骤中都使用 g++ 和 gold,程序会抛出 std::system_error
并提示您需要启用多线程。这是由于 --as-needed
标志。 GCC 默认将 --as-needed
传递给 linker,而 clang(显然)不会。使用 --as-needed
,linker 将只记录解析强引用的库依赖项。由于对 pthread API 的所有引用都很弱,其中 none 足以告诉 linker libpthread.so 应该添加到依赖列表(通过 DT_NEEDED
条目在动态 table)。改成clang或者加一个-Wl,--no-as-needed
标志就解决了这个问题,程序会加载pthread库
但是,等等,为什么在使用 Gnu linker 时不需要这样做?它使用相同的规则:只有强引用才会导致库被记录为依赖项。不同之处在于 Gnu ld 还考虑来自其他共享库的引用,而 gold 只考虑来自常规目标文件的引用。事实证明,pthread 库提供了几个 libc 符号的覆盖定义,并且 libstdc++.so
对其中一些符号(例如 write
)有强引用。这些强引用足以让 Gnu ld 将 libpthread.so
记录为依赖项。这与其说是设计,不如说是意外。我不认为更改 gold 以考虑来自其他共享库的引用实际上是一个可靠的修复。我认为正确的解决方案是 GCC 在使用 -pthread
.
--no-as-needed
放在 -lpthread
标志前面
这引出了一个问题,即为什么在使用 POSIX 线程和黄金 linker 时这个问题不会一直出现。但这是一个小测试程序;一个更大的程序几乎肯定会包含对 libpthread.so
覆盖的某些 libc 符号的强引用。
现在让我们看一下第二种失败模式,如果您 link libodr.so
使用 g++、gold 和 [=13=,Notify()
和 Get()
都会无限期地阻塞].
在 Notify()
中,您在调用 cv.notify_one()
时一直持有锁直到函数结束。你真的只需要持有锁来设置就绪标志;如果我们更改它以便在此之前释放锁,那么调用 Get()
的线程将在 300 毫秒后超时,并且不会阻塞。所以真正阻塞的是对 notify_one()
的调用,并且程序处于死锁状态,因为 Get()
正在等待同一个锁。
那为什么只有当__pthread_key_create
是FUNC
而不是NOTYPE
时才阻塞呢?我认为符号的类型是一个转移注意力的问题,真正的问题是 gold 没有记录由未添加为所需库的库解析的引用的符号版本。 wait_for
的实现调用了 pthread_cond_timedwait
,它在 libpthread
和 libc
中都有两个版本。加载器可能将引用绑定到错误的版本,导致无法解锁互斥体而导致死锁。我为 gold 做了一个临时补丁来记录这些版本,这让程序运行起来了。不幸的是,这不是解决方案,因为该补丁可能会导致 ld.so 在其他情况下崩溃。
我尝试将cv.wait_for(...)
更改为cv.wait(lock, []{ return ready; })
,程序在所有情况下都运行完美,这进一步表明问题出在pthread_cond_timedwait
。
底线是添加 --no-as-needed
标志将解决这个非常小的测试用例的问题。任何更大的东西都可能在没有额外标志的情况下工作,因为您将增加对 libpthread
中的符号进行强引用的可能性。 (例如,在 odr.cpp
中添加对 std::this_thread::sleep_for
anywhere 的调用会添加对 nanosleep
的强引用,从而将 libpthread
放入需要的清单。)
更新: 我已经确认失败的程序 link 正在访问 pthread_cond_timedwait
的错误版本。对于 glibc 2.3.2,pthread_cond_t
类型已更改,使用该类型的旧版本 API 已更改为动态分配新的(更大的)结构并在原始类型中存储指向它的指针。所以现在,如果消费线程在生产线程达到 cv.notify_one
之前达到 cv.wait_for
,cv.wait_for
的实现将调用旧版本的 pthread_cond_timedwait
,它会初始化它认为是cv
中的旧 pthread_cond_t
带有指向新 pthread_cond_t
的指针。之后,当另一个线程到达 cv.notify_one
时,它的实现假定 cv
包含一个新样式 pthread_cond_t
而不是指向一个的指针,因此它调用 pthread_mutex_lock
指向新 pthread_cond_t
的指针而不是指向互斥体的指针。它锁定了那个可能的互斥锁,但它永远不会被解锁,因为另一个线程解锁了真正的互斥锁。