C++全局构造和初始化
片段摘自程序员的自我修养—链接、装载与库.pdf
11.4
程序在进入main之前,需要对全局对象进行构造初始化。
glibc全局对象进行构造初始化
gibc启动程序时会经过.init
段,退出程序时会经过.finit
段。这两个段中的代码最终拼接成_init()
和_finit()
,这两个函数优先于/后于main
函数执行。
_start->_init()->main()->_finit()
用户所有放在.init
段的代码将在main
函数之前执行,即全局对象的初始化。具体执行流程如下:
_start->__libc_start_main->__libc_csu_init->_init->__do_global_ctors_aux
void __do_global_ctors_aux(void)
{
/*call constructor functions*/
unsigned long nptrs = (unsigned long)__CTOR_LIST__[0];
for(unsigned i = nptrs;i >= 1;i--)
__CTOR_LIST__[i]
}
CTOR_LIST 存放所有全局对象的构造和初始化的函数指针。__do_global_ctors_aux从__CTOR_LIST__
的下一个位置开始,按顺序执行函数指针,直到遇上NULL(__CTOR_END__
),从而调用所有全局对象的构造函数。
那么__CTOR_LIST__
是如何初始化的呢?
GCC编译器在编译的时候会遍历每个编译单元的全局对象,生成一个特殊函数_GLOBAL__I_HW
,利用这个特殊函数对本编译单元的所有全局、静态对象进行初始化。
static void _GLOBAL__I_HW(void)
{
Hw::Hw();//构造对象
atexit(__tcf_l)
}
一旦目标文件里面有这样的函数,编译器则会在这个编译单元目标文件的.ctors
放置一个指针,指向_GLOBAL__I_HW
。即__CTOR_LIST__
列表保存的是每个编译单元的_GLOBAL__I_HW
函数指针。链接器则会将所有的目标文件的.ctors
合并成一个.ctors段
__CTOR_LIST__
代表所有.ctors
的起始地址。
__CTOR_END__
指向.ctors
段的末尾。
所以如果想在main之前调用函数
,只需要在.ctors
段里面添加函数指针即可。
#include <stdio.h>
#include <stdlib.h>
//main函数之前执行
__attribute((constructor)) void before_main()
{
printf("====%s=====\n", __FUNCTION__);
}
//main函数之后执行
__attribute((destructor)) void after_main()
{
printf("====%s=====\n", __FUNCTION__);
}
int main(int argc, char **argv)
{
printf("=====entering %s.========\n", __FUNCTION__);
printf("====exiting from main!===\n");
return 0;
}
MSVC 全局对象进行构造初始化
msvc的入口函数mainCRTStartup
mainCRTStartup()
{
...
_initterm(__xc_a,__xc_z);
...
}
其中__xc_a
和__xc_z
是两个函数指针。而_initterm
的内容则是:
typedef void (__cdecl *_PVFV)();
static void __cdecl _initterm(_PVFV* pfbegin,_PVFV* pfend)
{
while(pfbegin < pfend)
{
if(*pfbegin != null)
(**pfbegin)();
++pfbegin;
}
}
第一眼看上去和glibc几乎一模一样,__xc_a
相当于指针的开始地址__CTOR_LIST__
,__xc_z
则相当于结束地址__CTOR_END__
那么msvc 是如何初始化__xc_a
和__xc_z
的呢?
_CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = {NULL};
_CRTALLOC(".CRT$XCZ") _PVFV __xc_Z[] = {NULL};
其中_CRTALLOC
定义
#pragma section(".CRT$XCA",long,read)
#pragma section(".CRT$XCZ",long,read)
#define _CRTALLOC(x) __declspec(allocate(x))
#pragma section("section-name",[,attributes])
的作用是在obj
的文件里创建名为section-name
的段
,并具有指定的attributes属性。所以上述两条指令实际生成了两个名为.CRT$XCA、.CRT$XCZ
的段。__declspec(allocate(x)
则表示变量将变量分配在段x
里。所以__xc_a
分配在.CRT$XCA
、__xc_z
分配在.CRT$XCZ
里。
msvc编译的时候,会为每一个编译单元生成.CRT$XCU
(U是User的意思)的段。在这个段里面加入自身的全局初始化函数。链接的时候将所有相同属性的段合并。段根据字母A-Z的顺序排序
。
所以如果想在main之前调用函数
,只需要在段(A-Z之间,不包含A,Z
).CRT$XCB
里面添加函数指针即可。
因为全局初始化函数在段
.CRT$XCU
里面,根据A-Z排序并执行。所以如果想要在全局构造函数之前执行,段名必须在U
之前。如果想在全局构造函数之后main之前则段名必须在U
之后。不包含A,Z
class CA
{
public:
CA(){printf("CA::CA\r\n");}
~CA(){printf("CA::~CA\r\n");}
};
#define SECNAME ".CRT$XCB"
#pragma section(SECNAME,long,read)
void foo()
{
printf("foo \r\n");
}
typedef void(_cdecl* _PVFV)();
_declspec(allocate(SECNAME)) _PVFV dummy[] = { foo };
CA g_obj;
int main()
{
printf("main enter\r\n");
printf("main exit\r\n");
return 0;
}
.CRT$XCB
段输出(先执行foo,在执行全局对象初始化,在执行main
)
.CRT$XCY
段输出(先执行全局对象初始化,在执行foo,在执行main
)
Note:注意事项,如果在main函数之前想引用全局变量或者函数,此时全局变量的构造函数还没执行。可能结果和预期不一致。
demo: foo里面改变CA成员变量的值。
CA g_obj;
void foo()
{
g_obj.m_a = 10;
printf("foo g_obj addr:%I64d g_obj.m_a=%d\r\n", &g_obj, g_obj.m_a);
}
int main()
{
printf("main enter\r\n");
printf("foo g_obj addr:%I64d g_obj.m_a=%d\r\n", &g_obj, g_obj.m_a);
printf("main exit\r\n");
return 0;
}
虽然在foo里面改变了m_a
,但是稍后执行段.CRT$XCU
时此时会调用CA::CA
导致m_a被重新赋值0