全局对象构造函数的调用时机(转载)
今天小翻了下新书《程序员的自我修养——链接、装载与库》中的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函数的调用。
我们知道,编译完的控制台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函数的调用。