MSVC CRT的全局构造和析构

来源:互联网 发布:javascript格式化输出 编辑:程序博客网 时间:2024/05/19 19:30

在了解了Glibc/GCC的全局构造析构之后,让我们趁热打铁来看看MSVC在这方面是如何实现的,有了前面的经验,在介绍MSVC CRT的全局构造和析构的时候使用相对简洁的方式,因为很多地方它们是相通的。

首先很自然想到在MSVC的入口函数mainCRTStartup里是否有全局构造的相关内容。我们可以看到它调用了一个函数为:

mainCRTStartup:
mainCRTStartup() 
{

_initterm( __xc_a, __xc_z );

}

其中__xc_a和__xc_z是两个函数指针,而initterm的内容则是:

mainCRTStartup -> _initterm:
// file: crt\src\crt0dat.c
static void __cdecl _initterm (_PVFV * pfbegin,_PVFV * pfend)
{
while ( pfbegin < pfend )
{
if ( *pfbegin != NULL )
(**pfbegin)();
++pfbegin;
}
}

其中_PVFV的定义是:

typedef void (__cdecl *_PVFV)();

从_PVFV的定义可以看出,它是一个函数指针类型,__xc_a和__xc_z则都是函数指针的指针。不过第一眼看到_initterm这个函数是不是看着很眼熟呢?对照Glibc/GCC的实现,_initterm长得可谓与__do_global_ctors_aux一模一样,它依次遍历所有的函数指针并且调用它们, __xc_a就是这个指针数组的开始地址,相当于__CTOR_LIST__;而__xc_z则是结束地址,相当于__CTOR_END__。

__xc_a和__xc_z不是mainCRTStartup的参数或局部变量,而是两个全局变量,它们的值在mainCRTStartup调用之前就已经正确地设置好了。我们知道mainCRTStartup作为入口函数是真正第一个执行的函数,那么MSVC是如何在此之前就将这两个指针正确设置的呢?让我们来看看__xc_a和__xc_z的定义:

// file: crt\src\cinitexe.c
_CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = { NULL };
_CRTALLOC(".CRT$XCZ") _PVFV __xc_z[] = { NULL };

其中宏_CRTALLOC 定义于crt\src\sect_attribs.h:

……
#pragma section(".CRT$XCA",long,read)
#pragma section(".CRT$XCZ",long,read)
……
#define _CRTALLOC(x) __declspec(allocate(x))

在这个头文件里,须要注意的是两条pragma指令。形如#pragma section的指令语法如下:

#pragma section( "section-name" [, attributes] )

作用是在生成的obj文件里创建名为section-name的段,并具有attributes属性。因此这两条pragma指令实际在obj文件里生成了名为.CRT$XCA和.CRT$XCZ的两个段。下面再来看看_CRTALLOC这个宏,该宏的定义为__declspec(allocate(x)),这个指示字表明其后的变量将被分配在段x里。所以__xc_a被分配在段.CRT$XCA里,而__xc_z被分配在段.CRT$XCZ里。

现在我们知道__xc_a和__xc_z分别处于两个特殊的段里,那么它是如何形成一个存储了初始化函数的数组呢?当编译的时候,每一个编译单元都会生成名为.CRT$XCU(U是User的意思)的段,在这个段中编译单元会加入自身的全局初始化函数。当链接的时候,链接器会将所有相同属性的段合并,值得注意的是:在这个合并过程中,所有输入的段在被合并到输出段时,是据字母表顺序依次排列。于是在本例中,各个段链接之后的状态可能如图11-11所示。

由于.CRT$XT*这些段的属性都是只读的,且它们的名字很相近,所以它们会被按顺序合并到一起,最后往往被放到只读段中,成为.rdata段的一部分。这样就自然地形成了存储所有全局初始化函数的数组,以供_initterm函数遍历。我们不得不再次惊叹!MSVC CRT的全局构造实现在机制上与Glibc基本是一样的,只不过它们的名字略有不同,MSVC CRT采用这种段合并的模式与.ctor的合并及__CTOR_LIST__和__CTOR_END__的地址确定何其相似!这再一次证明了虽然各个操作系统、运行库、编译器在细节上大相径庭,但是在基本实现的机制上其实是完全相通的。

 (点击查看大图)图11-11  PE文件的初始化部分

【小实验】

自己添加初始化函数:

#include <iostream>
#define SECNAME ".CRT$XCG"
#pragma section(SECNAME,long,read)
void foo()
{
std::cout << "hello" << std::endl;
}
typedef void (__cdecl *_PVFV)();
__declspec(allocate(SECNAME)) _PVFV dummy[] = { foo };
int main()
{
return 0;
}

0 0