填坑日记:linux下动态链接库(.so)的构造(constructor)中使用静态对象导致程序崩溃

最近按项目的要求封装了一个动态库。本来没啥毛病,中间为了方便调试,加了日志功能(使用了static对象作为单例),并在constructor中执行了日志相关设置。编译ok,调用的时候在dlopen报错崩溃。

坑的描述 linux下动态链接库(.so)的constructor中调用了static对象的相关功能,导致调用库时崩溃
根本原因 constructor是在dlopen返回之前调用的,此时可能还未对全局或静态对象执行构造
填坑进度 已解决

问题描述:

so工程中,增加了写日志的功能。日志模块是已写好的一个模块,可以使用单线程或者多线程写本地文件的方式实现写日志的功能;日志模块内部实现了日志信息的格式化输出;日志模块包括一个核心功能类,但是一般情况不直接调用类函数,而是通过宏定义执行调用;通过宏定义,实际上使用了日志模块核心类的一个静态单例对象;该模块已在众多项目(包括windows和linux)使用,稳定可靠。调用代码如下所示:

[cpp]
void __attribute__((constructor)) Constructor()
{
string strLogFolderPath("/tmp/VideoProLog/");
if(CFilePro::exists(strLogFolderPath.c_str()))
{
__Z_START_LOG__(strLogFolderPath.c_str());
__Z_LOG_WITH_FUNCNAME_A__("create the world");
}
}

void __attribute__((destructor)) Destructor()
{
ReleaseDevice();
__Z_LOG_WITH_FUNCNAME_A__("destroy the world");
__Z_STOP_LOG__(true);
}
[/cpp]

其中,宏定义如下:

[cpp]
#ifndef __Z_START_LOG__
#define __Z_START_LOG__(A) CLogWriter::GetInstance()->StartLog(A, false);
#endif

#ifndef __Z_STOP_LOG__
#define __Z_STOP_LOG__(A) CLogWriter::GetInstance()->StopLog(A);
#endif

#ifndef __Z_LOG_WITH_FUNCNAME_A__
#define __Z_LOG_WITH_FUNCNAME_A__(A, ...) {CLogWriter::GetInstance()->WriteLogWithFuncNameExA(__FUNCTION__, __LINE__, A, ##__VA_ARGS__);}
#endif
[/cpp]

原因分析:

  1. 注释Constructor()中日志相关代码,重新编译后行为正常,可确证崩溃是由日志模块造成。
  2. 本项目中未开启多线程日志,可以排除是多线程的影响。
  3. CLogWriter::GetInstance()实际指向一个静态日志核心功能类对象的指针。在此处替换为一个普通日志核心功能类的对象,重新编译后行为正常。由此可推出该问题与静态对象有关。
  4. 将日志部分移出Constructor()以外,保留CLogWriter::GetInstance()的写法,重新编译后行为正常。由此可推出该问题与Constructor()有关。

gcc手册6.33.1 Common Function Attributes 找到相关描述如下:

The constructor attribute causes the function to be called automatically before execution enters main (). Similarly, the destructor attribute causes the function to be called automatically after main () completes or exit () is called. Functions with these attributes are useful for initializing data that is used implicitly during the execution of the program.

On some targets the attributes also accept an integer argument to specify a priority to control the order in which constructor and destructor functions are run. A constructor with a smaller priority number runs before a constructor with a larger priority number; the opposite relationship holds for destructors. Note that priorities 0-100 are reserved. So, if you have a constructor that allocates a resource and a destructor that deallocates the same resource, both functions typically have the same priority. The priorities for constructor and destructor functions are the same as those specified for namespace-scope C++ objects (see C++ Attributes). However, at present, the order in which constructors for C++ objects with static storage duration and functions decorated with attribute constructor are invoked is unspecified. In mixed declarations, attribute init_priority can be used to impose a specific ordering.

Using the argument forms of the constructor and destructor attributes on targets where the feature is not supported is rejected with an error.

意思就是说,如果一个函数被设定为constructor属性(例如上述的Constructor()函数),则该函数会在main()函数执行之前被自动的执行,对应于.so就是在外部调用dlopen时起作用;如果一个函数被设定为destructor属性(例如上述的Destructor()函数),则该函数会在main()函数执行之后或者exit()被调用后被自动的执行,对应于.so就是在外部调用dlclose时起作用。被constructor属性修饰的函数,可以设置0~100的优先级,数值越小优先级越高,会被更早地执行。默认情况下,静态变量和构造函数修饰调用顺序是未指定的。Constructor()作为.so的入口点,在外部载入.so库的第一时间执行;而这个时候,可能还无法对全局或静态对象执行构造。

解决方案:

不要在使用constructor标记的函数中,调用全局或者静态对象;或者将静态对象的初始化在优先级更高的构造函数中执行。

 

二零二二年四月二日 顾毅写于厦门

二零二二年四月四日 修订