【发布时间】:2021-04-13 15:09:11
【问题描述】:
我正在努力学习和理解如何为 Linux 开发共享库。但是,我遇到了一个我无法解释的段错误。我想知道这是否是在共享库中实现构造函数的正确方法。我的模型库的源代码如下。
helper.h:
#ifndef HELPER_HELPER_H
#define HELPER_HELPER_H
namespace helper {
int GetHelp(int i=0);
} // namespace helper
#endif // HELPER_HELPER_H
helper.cxx:
#include "helper.h"
#include <iostream>
#include <string>
namespace helper {
void __attribute__((constructor)) InitHelp(void)
{
std::cout << "Setting up Helper" << std::endl;
}
void __attribute__((destructor)) FinishHelp(void)
{
std::cout << "Leaving Helper" << std::endl;
}
int GetHelp(int i)
{
return 2*i+1;
}
} // namespace helper
main.cxx:
#include <iostream>
#include <cstdlib>
#include "helper.h"
int main(int argc, char** argv)
{
std::cout << "Hello World" << std::endl;
const int i = 3;
std::cout << "GetHelp returned " << helper::GetHelp(i) << std::endl;
std::cout << "Good bye World" << std::endl;
return EXIT_SUCCESS;
}
我已将它们放在单独的子目录中,并添加了最简单的 cmake 文件。
根目录:
cmake_minimum_required(VERSION 3.19)
project(MyProject VERSION 1.2.3
DESCRIPTION "Very nice project"
LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_subdirectory(libhelper)
add_subdirectory(pdemo)
libhelper 目录:
add_library(helper)
target_sources(helper PUBLIC helper.h
PRIVATE helper.cxx)
target_include_directories(helper PUBLIC .)
install(TARGETS helper)
pdemo目录:
add_executable(pdemo)
target_sources(pdemo PRIVATE main.cxx)
target_link_libraries(pdemo PRIVATE helper)
install(TARGETS pdemo)
创建构建目录后,设置
cmake -DCMAKE_BUILD_TYPE='Debug' -DBUILD_SHARED_LIBS=ON ..
和编译,可执行文件产生一个段错误。从核心文件中使用 gdb 进行追溯,结果发现问题出现在 initHelp 函数中,位于 libstdc++ 深处。通过删除std::cout << "Setting up ... 行,问题消失了,程序产生了预期的输出。我已经使用 GCC 5.4.0 和 GCC 10.2.0 对其进行了测试,并且都遇到了同样的问题。奇怪的是,如果开启 LTO,问题就会消失。
我希望在初始化可执行文件期间调用构造函数时已经设置了所有支持库,包括 C++ 标准库。是这样吗?如果是,是什么导致了段错误?我是否误解了构造函数的使用方式?
(注意:我知道我可以通过将库构造函数移动到 C++ 类的构造函数并从该类实例化一个全局对象来产生相同的结果。我猜编译器可能会使用相同的机制和一些额外的步骤我不知道。但是,我想了解发生了什么以及实现共享库的真正规则是什么。)
更新:
在阅读了 KamilCuk 的答案后,我开始思考并得出结论,事情比我以前想象的要糟糕得多。考虑以下程序:
#include <iostream>
#include <cstdlib>
class Foo
{
public:
Foo() {
std::cout << "This is Foo speaking" << std::endl; }
};
Foo global_foo;
int main(int argc, char** argv)
{
std::cout << "Hello World" << std::endl;
return EXIT_SUCCESS;
}
根据我们之前讨论的内容,这个看似简单的生物是无效的 C++ 代码。很明显,如果静态链接到libstdc++,那么就是静态初始化顺序失败,因为不能保证std::cout等会在global_foo之前构造。如果它是动态链接的,则不能保证正确的初始化顺序,因为共享对象和可执行文件的加载过程没有定义任何预定的库初始化完成顺序,并且库构造函数在加载过程的这个阶段运行保证不会初始化其他库。实际上,这意味着静态初始化顺序惨败过度扩展了链接单元(.so 和可执行文件)的边界。
从实际的角度来看,这意味着在 C++ 程序中实例化静态对象必须非常小心。开发人员必须确保构造函数中直接或间接发生的任何事情都不会触及该类控制之外的任何东西,不会触及其他库中的设施,不涉及全局变量等。保守地说,最好的方法是完全避免将对象作为全局变量。 (我知道,避免全局变量总是很重要,但有时一些例外是必要的。比如 std::cout :))
此外,为 C 或 C++ 设计一个共享库,库构造函数不能依赖于该库所依赖的任何其他库。
我理解得好吗?顺便问一下,MS Windows dll 是否具有相同的属性?
【问题讨论】:
-
Is this the case?没有。what's going onstd::cout在你的库之后初始化,仅此而已。what are the real rules只是不要在构造函数中使用全局变量或确保首先构造这些对象。研究“静态初始化顺序惨败”。 -
亲爱的 KamilChuk,谢谢。我有点意外,因为 SIOF 貌似是一个 C++ 问题,即不同翻译单元之间的静态变量初始化的任意顺序。上例是不同库.so文件之间的初始化顺序,所以这是一个动态链接问题。假设 SIOF 也适用于此,在设置我的库时未初始化 libstdc++。如果我在设置时不能依赖我的库所依赖的任何库,如果可能需要调用这些库,我该如何设置自己的库?
标签: c++ linux constructor shared-libraries