1. 问题背景
log4cxx是C 常用的log库。
在项目中碰到程序启动后偶尔很快就crash,查看函数调用栈后,居然在log4cxx的模块。对于常用的开源库,笔者一般还是比较放心的,于是目光一直聚焦在产品的代码,搜寻无果后,只能去看看一看log4cxx的源码了,果不其然,最终寻得是log4cxx的一个多线程bug所致,而这个bug和C 函数内的static变量是否线程安全有关。环境相关信息如下:
- 编译器: VS2005
- log4cxx当时最新版本是0.10.0
项目中会调用到log4cxx的getWarn这个接口,如下代码所示,由于这个函数存在非线程安全的问题,导致程序Crash。为了更好的描述问题,博主下一节采用一个简单的例子去做分析:为什么这个是非线程安全的。
代码语言:javascript复制LevelPtr Level::getWarn() {
static LevelPtr level(new Level(Level::WARN_INT, LOG4CXX_STR("WARN"), 4));
return level;
}
2. 通过样例代码分析问题
这里们写了一段样例代码,模拟Log4cxx上述代码。采用VS2005编译,为了避免程序被优化,博主采用的是Debug模式编译。
代码语言:javascript复制class TestObject
{
public:
int m_iVal;
TestObject()
{
m_iVal = 4;
}
};
TestObject TestFunction()
{
static TestObject obj;
return obj;
}
以上代码简单来说,就是返回一个TestObject的类对象。TestFunction中永远返回一个静态对象obj。
代码语言:javascript复制TestObject TestFunction()
{
0000000140001800 mov qword ptr [rsp 8],rcx
0000000140001805 push rdi
0000000140001806 sub rsp,30h
000000014000180A mov rdi,rsp
000000014000180D mov rcx,0Ch
0000000140001817 mov eax,0CCCCCCCCh
000000014000181C rep stos dword ptr [rdi]
000000014000181E mov rcx,qword ptr [rsp 40h]
0000000140001823 mov qword ptr [rsp 20h],0FFFFFFFFFFFFFFFEh
static TestObject obj;
//===========================
这个地方从内存中读取一个值,可以理解为编译器给程序自动加了一个变量bInit
(判断obj对象是否初始化了,bInit初始值为0)
(1)将bInit读取到eax,然后判断为1表示已经初始化,则直接返回对象;
(2)如果为0,则按顺序继续执行。
//===========================
000000014000182C mov eax,dword ptr [$S1 (14000F2A4h)]
0000000140001832 and eax,1
0000000140001835 test eax,eax
0000000140001837 jne TestFunction 55h (140001855h)
//===========================
将bInit值设置为1, 并且调用obj构造函数, 完成对象初始化
//===========================
0000000140001839 mov eax,dword ptr [$S1 (14000F2A4h)]
000000014000183F or eax,1
0000000140001842 mov dword ptr [$S1 (14000F2A4h)],eax
0000000140001848 lea rcx,[obj (14000F2A0h)]
000000014000184F call TestObject::TestObject (1400011EFh)
0000000140001854 nop
return obj;
0000000140001855 mov rax,qword ptr [rsp 40h]
000000014000185A mov ecx,dword ptr [obj (14000F2A0h)]
0000000140001860 mov dword ptr [rax],ecx
0000000140001862 mov rax,qword ptr [rsp 40h]
}
看了以上汇编和解释之后,大家应该能明白这里存在一个Race Condition。当多个线程,同时调用TestFunction这个函数,当线程A刚执行完0000000140001842 mov dword ptr [$S1 (14000F2A4h)],eax (第28行), 线程B刚好进入TestFunction执行,认为obj已经初始化了,则直接返回对象,而此时对象内部的m_iVal还未进入构造函数内赋值为4, 将会使用错误的值继续执行代码,并非程序的本意。
3. C 11 线程安全
博主采用了VS2015 (支持C 11)编译了以上的代码,得到如下汇编, 其通过_Init_thread_header和_Init_thread_footer来保证局部的静态对象的初始化线程安全。
代码语言:javascript复制TestObject TestFunction()
{
00007FF65F411830 mov qword ptr [rsp 8],rcx
00007FF65F411835 push rbp
00007FF65F411836 push rdi
00007FF65F411837 sub rsp,108h
00007FF65F41183E lea rbp,[rsp 20h]
00007FF65F411843 mov rdi,rsp
00007FF65F411846 mov ecx,42h
00007FF65F41184B mov eax,0CCCCCCCCh
00007FF65F411850 rep stos dword ptr [rdi]
00007FF65F411852 mov rcx,qword ptr [rsp 128h]
00007FF65F41185A mov qword ptr [rbp 0C8h],0FFFFFFFFFFFFFFFEh
static TestObject obj;
00007FF65F411865 mov eax,104h
00007FF65F41186A mov eax,eax
00007FF65F41186C mov ecx,dword ptr [_tls_index (07FF65F41C1E0h)]
00007FF65F411872 mov rdx,qword ptr gs:[58h]
00007FF65F41187B mov rcx,qword ptr [rdx rcx*8]
00007FF65F41187F mov eax,dword ptr [rax rcx]
00007FF65F411882 cmp dword ptr [obj 4h (07FF65F41C180h)],eax
00007FF65F411888 jle TestFunction 88h (07FF65F4118B8h)
00007FF65F41188A lea rcx,[obj 4h (07FF65F41C180h)]
00007FF65F411891 call _Init_thread_header (07FF65F41101Eh)
00007FF65F411896 cmp dword ptr [obj 4h (07FF65F41C180h)],0FFFFFFFFh
00007FF65F41189D jne TestFunction 88h (07FF65F4118B8h)
00007FF65F41189F lea rcx,[obj (07FF65F41C17Ch)]
00007FF65F4118A6 call TestObject::TestObject (07FF65F411028h)
00007FF65F4118AB nop
00007FF65F4118AC lea rcx,[obj 4h (07FF65F41C180h)]
00007FF65F4118B3 call _Init_thread_footer (07FF65F411078h)
return obj;
00007FF65F4118B8 mov rax,qword ptr [rbp 100h]
00007FF65F4118BF mov ecx,dword ptr [obj (07FF65F41C17Ch)]
00007FF65F4118C5 mov dword ptr [rax],ecx
00007FF65F4118C7 mov rax,qword ptr [rbp 100h]
}
00007FF65F4118CE lea rsp,[rbp 0E8h]
00007FF65F4118D5 pop rdi
00007FF65F4118D6 pop rbp
00007FF65F4118D7 ret
这个功能在VS2015中默认开启,如果想要禁用这个功能, 可以添加额外的编译选项/Zc:threadSafeInit-。详细的可以参考/Zc:threadSafeInit (Thread-safe Local Static Initialization)。
4. 总结
1. C 11之前函数内部static变量非线程安全。
2. 尽量在条件允许的情况下,将编译器升级到支持C 11。