在了解了Glibc/GCC的全局构造析构之后,让我们趁热打铁来看看MSVC在这方面是如何实现的,有了前面的经验,在介绍MSVC CRT的全局构造和析构的时候使用相对简洁的方式,因为很多地方它们是相通的。
首先很自然想到在MSVC的入口函数mainCRTStartup里是否有全局构造的相关内容。我们可以看到它调用了一个函数为:
mainCRTStartup: mainCRTStartup() |
其中__xc_a和__xc_z是两个函数指针,而initterm的内容则是:
mainCRTStartup -> _initterm: // file: crt\src\crt0dat.c |
其中_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\src\sect_attribs.h:
…… |
在这个头文件里,须要注意的是两条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" int main() |