MSVC CRT的全局构造和析构(1)

11.4.2 MSVC CRT的全局构造和析构(1)

11.4.2  MSVC CRT的全局构造和析构(1)

在了解了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;
}
【责任编辑: 云霞 TEL:(010)68476606】


回书目   上一节   下一节

11.4.2 MSVC CRT的全局构造和析构(2)
http://book.51cto.com  2009-04-22 16:12  俞甲子/石凡/潘爱民  电子工业出版社  我要评论(0)

    * 摘要:《程序员的自我修养:链接、装载与库》第11章运行库。本章主要介绍运行库的概念、C/C++运行库、Glibc和MSVC CRT、运行库如何实现C++全局构造和析构及以fread()库函数为例对运行库进行剖析。本节为大家介绍MSVC CRT的全局构造和析构。
    * 标签:MSVC CRT  程序员  自我修养  程序员的自我修养:链接、装载与库
    *
      Oracle帮您准确洞察各个物流环节

11.4.2  MSVC CRT的全局构造和析构(2)

运行这个程序,可以得到如"hello"的输出。为了验证A~Z的这个字母表排列,读者可以修改SECNAME,使之不处于.CRT$XCA 和.CRT$XCZ之间,理论上不会得到任何输出。而如果将段名改为.CRT$XCV(V的字典序在U之后),那么foo函数将在main执行之后执行。

MSVC CRT 析构

最后来看看MSVC的全局析构的实现,在MSVC里,只需要在全局变量的定义位置上设置一个断点,就可以看到在.CRT$XC?中定义的全局初始化函数的内容。我们仍然使用本章一开头的HelloWorld来作为示例:

#include <iostream>
class HelloWorld
{
public:
HelloWorld() {std::cout << "hi/n";}
~HelloWorld(){std::cout << "bye/n";}
};
HelloWorld Hw;
int main()
{
return 0;   
}

这里在加粗的位置上设置断点。运行程序并中断之后查看反汇编可以得到初始化函数的内容:

011B1B70 mov eax,dword ptr [__imp_std::cout (11B2054h)]
011B1B75 push offset string "hi/n" (11B2124h)
011B1B7A push eax 
011B1B7B call std::operator<<<std::char_traits<char> > (11B1140h)
011B1B80 push offset `dynamic atexit destructor for 'Hw'' (11B1B90h)
011B1B85 call atexit (11B13B0h)
011B1B8A add esp,0Ch
011B1B8D ret

在这里可以看见这段程序首先调用了内联之后的HelloWorld的构造函数,然后和 g++相同,调用atexit将一个名为dynamic atexit destructor for 'Hw''的函数注册给程序退出时调用。而这个dynamic atexit destructor for 'Hw''函数的定义也能很容易找到:

`dynamic atexit destructor for 'Hw'':
011B1B90 mov eax,dword ptr [__imp_std::cout (11B2054h)]
011B1B95 push  offset string "bye/n" (11B2128h)
011B1B9A push eax 
011B1B9B call std::operator<<<std::char_traits<char> > (11B1140h)
011B1BA0 add esp,8
011B1BA3 ret

可以看出,这个函数的作用就是在对象Hw调用内联之后进行析构。看到这里,我想各位读者肯定有跟我一样的心情,那就是希望举一反三的愿望并不是不切实际的,它是实实在在存在的。Glibc下通过__cxa_exit()向exit()函数注册全局析构函数;MSVC CRT也通过atexit()实现全局析构,它们除了函数命名不同之外几乎没有区别。
【责任编辑:云霞 TEL:(010)68476606】

回书目   上一节   下一节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值