我们都知道,如果在一个.cpp文件中定义了一系列的全局对象,这些对象的初始化顺序(即,他们的.ctor的调用顺序)是和他们的定义顺序一致的,我们可以很容易的知道哪些对象先被实例化,从而不知不觉的依赖于这种顺序来编码。然而如果是在几个.cpp文件中定义全局对象,此时他们的初始化顺序呢?C++标准并没有对这个顺序作任何声明,只是简单要求全局对象必须在main函数进入之前初始化完毕。那诸如以下代码,我们如何保证他能够被正确执行呢?
Class A { // 不应该写出这样的类型,不尊从C++语义public:A() { cout << "In A’s Constructor…" << endl; }~A() { cout << "In A’s Destructor…" << endl; } };A aaaa; // 这里定义了一个全局的aaaa对象
我们经常很随意的就写出了这样的一个类型,殊不知cout和aaaa的初始化顺序,会影响代码的正确执行。我们本能的做出这样的假设,为什么cout就先于aaaa被初始化呢?基于这种假设的实现依赖于编译器+连接器,不同的平台实现稍有差异,但是始终大同小异。详见《Inside the C++ Object Model》。
对于VC++的实现方式,是通过两点完成的。CRT Startup Code和预处理指令#pregma:
#pragma init_seg({ compiler | lib | user | "section-name" [, func-name]} )
[In MSDN: Specifies a keyword or code section that affects the order in which startup code is executed. ]
compiler/lib/user/section_name分别定义了初始化顺序的优先级别,compiler为最优先,一般为CRT库保留使用。我们可以在cout所在的.cpp文件中找到#pragma init_seg(compiler)的声明。CRT中的实现呢?
/* crtexe.c
* do C++ constructors (initializers) specific to this EXE
*/
if (__native_startup_state == __initializing)
{
_initterm( __xc_a, __xc_z );
__native_startup_state = __initialized;
}
在__tmainCRTStartup的定义中可以发现如上的代码,他的工作就是遍历一个函数指针列表,依次进行调用。这些函数指针就指向了那个全局对象的构造函数,当然也有可能是其他的初始化用意的函数。那么这个函数指针列表是如何被构造出来的呢?这就要借助于连接器和编译器了。VC++编译器和CRT有一个约定:当VC++编译器遇到全局对象的初始化器以及内存释放工作(如:构造函数&析构函数),他就会产生一个dynamic initializer,并把他置于.obj文件的.CRT$XCU段中。.CRT是section的名称,$后面的XCU是group名称。(关于COFF或者PE的详细内容,请关注Matt大拿的Columns)。从init_seg的sample code所编译得到的代码中,使用dumpbin /all ctor.obj /out:d:\ctor.txt可以发现这个段中的内容如下:
SECTION HEADER #22
.CRT$XCU name
0 physical address
0 virtual address
4 size of raw data
21B8 file pointer to raw data (000021B8 to 000021BB)
21BC file pointer to relocation table
0 file pointer to line numbers
1 number of relocations
0 number of line numbers
40300040 flags
Initialized Data
4 byte align
Read Only
RAW DATA #22
00000000: 00 00 00 00 ….
RELOCATIONS #22
Symbol Symbol
Offset Type Applied To Index Name
——– —————- —————– ——– ——
00000000 DIR32 00000000 42 ??__Eaaaa@@YAXXZ (void __cdecl `dynamic initializer for ‘aaaa”(void))
这就是一个编译器为aaaa产生的dynamic initializer,??__Eaaaa@@YAXXZ 是被name mangling后的名称(详见calling convention)。
另外编译器还产生了两个section,其中个包含了一个变量用作这个列表的哨兵:
__xc_a in .CRT$XCA__xc_z in .CRT$XCZ //__xc_a和__xc_z就是刚才CRT Startup Code中传给_initterm的两个参数
OK。当连接器将各个同名的section进行归并,同时按照$后面的group name进行排序。那么将来被windows loader加载进内存后也会依旧保持这个顺序。那么我们的全局对象初始化器的dynamic initializer也会以这个顺序被调用。
_initterm 的汇编代码:
023C010 mov edi,edi1023C012 push ebp1023C013 mov ebp,esp1023C015 mov eax,dword ptr [ebp+8]1023C018 cmp eax,dword ptr [ebp+0Ch]1023C01B jae __initterm+27h (1023C037h)1023C01D mov ecx,dword ptr [ebp+8]1023C020 cmp dword ptr [ecx],01023C023 je __initterm+1Ch (1023C02Ch)1023C025 mov edx,dword ptr [ebp+8]1023C028 mov eax,dword ptr [edx]1023C02A call eax // 在这里调用了每个dynamic initializer1023C02C mov ecx,dword ptr [ebp+8]1023C02F add ecx,41023C032 mov dword ptr [ebp+8],ecx1023C035 jmp __initterm+5 (1023C015h)1023C037 pop ebp1023C038 ret
dynamic initializer:
004146F0 push ebp
004146F1 mov ebp,esp
004146F3 sub esp,0C0h
004146F9 push ebx
004146FA push esi
004146FB push edi
004146FC lea edi,[ebp-0C0h]
00414702 mov ecx,30h
00414707 mov eax,0CCCCCCCCh
0041470C rep stos dword ptr es:[edi]
0041470E mov ecx,offset aaaa (419484h)
00414713 call A::A (4110EBh) // 真正调用了对象的构造函数
00414718 push offset `dynamic atexit destructor for ‘aaaa” (415810h)
0041471D call @ILT+100(_atexit) (411069h)
00414722 add esp,4
00414725 pop edi
00414726 pop esi
00414727 pop ebx
00414728 add esp,0C0h
0041472E cmp ebp,esp
00414730 call @ILT+340(__RTC_CheckEsp) (411159h)
00414735 mov esp,ebp
00414737 pop ebp
00414738 ret
同时注意以上的_atexit的调用。dynamic initializer为对象aaaa又注册了一个dynamic atexit destructor,用来在程序退出的时候由atexit调用以释放对象的内存。atexit:
对于MSDN中的那段sample code还有一点需要说明:InitSegStart和InitSegEnd都是在这个哨兵section中的变量,在后面的代码中只是用到了他们的地址。他们就相当于上面的__xc_a和__xc_z
#pragma section(".mine$a", read)
__declspec(allocate(".mine$a")) const PF InitSegStart = (PF)1;#pragma section(".mine$z",read)
__declspec(allocate(".mine$z")) const PF InitSegEnd = (PF)1;
小节
最近在重构ExUnitTest的代码,同时也在Review代码,发现了不少Singleton使用不当的地方。这些Singleton严重的基于了一定的初始化顺序的约束,所以每次我手工调换FreeInstance的位置时,进程就会Crash。当然这样的问题在任何C++项目中应该都是比较普遍的,在我们的项目中比较好的地方就是这些个类的实例化以及释放都被Facade封装了,我们只需调用expose出来的interface就可以,不用关心这些依赖性。
最后讲个额外的话题,boss要让我做ACRD Team这边的Build Engineer接手Chris Canndy一部分工作,感觉担子有点重,毕竟是把关的position。不过只要做两个月,之后会招一个intern,让intern来接管这个工作。由于最近公司预算的问题,intern的职位需要等到4月以后才能下来。这个position是build engineer & developer,需要较好的development skill,因为我们将来正在考虑实现自动编译部署,所以编程技能不能忽略。有机会看到我这个post的同僚可以发我简历,没看见的哥们就算你可惜了。哈哈~