全局对象构造函数的调用时机

今天小翻了下新书《程序员的自我修养——链接、装载与库》中的11.4.2章节的《MSVC CRT的全局构造与析构》部分。整体而言,作者对于全局函数的调用时机阐述得比较清楚,但其中有一点疑问,作者并没有写清楚,这里我就补充下。(以下讨论的是Windows平台,linux类似。)

      我们知道,编译完的控制台exe文件一般情况下并不是从main函数执行的,也就是说pe文件头的入口点并不是指向main函数的,而是指向一个叫做mainCRTStartup的启动函数。该函数在vc安装目录的crt/src/crtexe.c中实现。具体如下:

void mainCRTStartup(void)
{

          ......
        _initterm( __xi_a, __xi_z ); //这个函数进行全局对象构造函数的初始化调用代码


          ......           

        _initterm( __xc_a, __xc_z );

         ......

}

     其中,_initterm( __xi_a, __xi_z ); 是全局对象构造函数的初始化调用代码。我们来看下它的具体实现:

void __cdecl _initterm ( _PVFV * pfbegin,_PVFV * pfend)
{
                while ( pfbegin < pfend )
        {
             if ( *pfbegin != NULL )
                (**pfbegin)();
            ++pfbegin;
        }
}

     从上面的代码中我们可以看到,全局对象的初始化函数被调用为void fn(void)类型。这就有个疑问了,看下面代码:#include <stdio.h>
void gfn ();

class A {
          int a;
   public:
         A (char* a);
};
A::A(char* a)
{
     printf ("global!/n");
}
 A a("sapair");
 int main ()
 {
     printf ("main!/n");
 } 

  上面这段代码全局对象构造函数并不是无参类型,那按照_initterm 的实现,我们知道_initterm 调用的初始化函数是无参类型的,那A的有参构造函数又是如何在main函数之前被调用的呢?下面我们来解密这个问题:

   用OD载入编译后的代码,可以找到下面的反汇编片段:

 004010B0 >/$  55             push    ebp
  004010B1  |.  8BEC          mov     ebp, esp
  004010B3  |.  83EC 40       sub     esp, 40
  004010B6  |.  53            push    ebx
  004010B7  |.  56            push    esi
  004010B8  |.  57            push    edi
  004010B9  |.  8D7D C0       lea     edi, dword ptr [ebp-40]
  004010BC  |.  B9 10000000   mov     ecx, 10
  004010C1  |.  B8 CCCCCCCC   mov     eax, CCCCCCCC
  004010C6  |.  F3:AB         rep     stos dword ptr es:[edi]
  004010C8  |.  68 28204200   push    00422028                         
                                     ;  ASCII "sapair"
  004010CD  |?  B9 587D4200   mov     ecx, offset a
  004010D2  |?  E8 2EFFFFFF   call    00401005 ;就是A的有参构造函数
  004010D7  |.  5F            pop     edi
  004010D8  |?  5E            pop     esi
  004010D9  |?  5B            pop     ebx
  004010DA  |.  83C4 40       add     esp, 40
  004010DD  |?  3BEC          cmp     ebp, esp
  004010DF  |?  E8 EC000000   call    _chkesp
  004010E4  /.  8BE5          mov     esp, ebp
  004010E6      5D            pop     ebp
  004010E7      C3            retn

  此时观察堆栈窗口,有调用回溯,我们可以转到调用代码处:

 00402F90 >/$  55             push    ebp
  00402F91  |.  8BEC          mov     ebp, esp
  00402F93  |>  8B45 08       mov     eax, dword ptr [ebp+8]
  00402F96  |.  3B45 0C       cmp     eax, dword ptr [ebp+C]
  00402F99  |.  73 18         jnb     short 00402FB3
  00402F9B  |.  8B4D 08       mov     ecx, dword ptr [ebp+8]
  00402F9E  |.  8339 00       cmp     dword ptr [ecx], 0
  00402FA1  |.  74 05         je      short 00402FA8
  00402FA3  |.  8B55 08       mov     edx, dword ptr [ebp+8]
  00402FA6  |.  FF12          call    dword ptr [edx]
  00402FA8  |>  8B45 08       mov     eax, dword ptr [ebp+8]
  00402FAB  |.  83C0 04       add     eax, 4
  00402FAE  |.  8945 08       mov     dword ptr [ebp+8], eax
  00402FB1  |.^ EB E0         jmp     short 00402F93
  00402FB3  |>  5D            pop     ebp
  00402FB4  /.  C3            retn

 上面这段反汇编代码是不是有点眼熟?呵呵,不错的,这段反汇编就是_initterm( __xi_a, __xi_z )的实现:)

  由此,我们总结了下原因:

  原来,编译器对于全局对象的非无参构造函数进行了一个包装,把对有参构造函数的调用进行了一个封装,封装为一个无参类型的函数,然后把这个无参类型的函数作为_initterm函数的调用。

你可能感兴趣的:(其他)