片段摘自程序员的自我修养—链接、装载与库.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
4393

被折叠的 条评论
为什么被折叠?



