Global Object Initializers

我们都知道,如果在一个.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,edi
1023C012  push        ebp 
1023C013  mov         ebp,esp
1023C015  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],0
1023C023  je          __initterm+1Ch (1023C02Ch)
1023C025  mov         edx,dword ptr [ebp+8]
1023C028  mov         eax,dword ptr [edx]
1023C02A  call        eax  // 在这里调用了每个dynamic initializer
1023C02C  mov         ecx,dword ptr [ebp+8]
1023C02F  add         ecx,4
1023C032  mov         dword ptr [ebp+8],ecx
1023C035  jmp         __initterm+5 (1023C015h)
1023C037  pop         ebp 
1023C038  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:

int __cdecl atexit ( _PVFV func  ) {
        return (_onexit((_onexit_t)func) == NULL) ? -1 : 0; }

对于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的同僚可以发我简历,没看见的哥们就算你可惜了。哈哈~