CRT 初始化
本文介绍 CRT 如何在本机代码中初始化全局状态。
默认情况下,链接器包括 CRT 库,此库提供其自己的启动代码。 此启动代码初始化 CRT 库,调用全局初始值设定项,然后调用用于控制台应用程序的用户提供的 main
函数。
虽然不建议,但可以使用特定于 Microsoft 的链接器行为,以特定顺序插入自己的全局初始值设定项。 此代码不可移植,并且有一些重要的注意事项。
初始化全局对象
请考虑以下 C++ 代码(C 不允许此代码,因为它不允许常数表达式中的函数调用)。
int func(void)
{
return 3;
}
int gi = func();
int main()
{
return gi;
}
根据 C/C++ 标准,在执行 main()
前必须调用 func()
。 但哪个项来调用它呢?
确定调用方的一个方法是在 func()
中设置断点,调试应用程序并检查堆栈。 由于 CRT 源代码是 Visual Studio 附带的,因此可以这样做。
你在浏览堆栈上的函数时,会看到 CRT 正在调用函数指针列表。 这些函数类似于 func()
或类实例的构造函数。
CRT 从 Microsoft C++ 编译器中获取函数指针的列表。 当编译器发现全局初始值时,它将在 .CRT$XCU
部分(其中,CRT
是部分名称,XCU
是组名称)中生成一个动态初始值设定项。 要获取动态初始值设定项列表,请运行命令 dumpbin /all main.obj
,然后搜索 .CRT$XCU
部分。 仅当 main.cpp
编译为 C++ 文件而非 C 文件时,该命令才适用。 它应当与以下示例类似:
SECTION HEADER #6
.CRT$XCU name
0 physical address
0 virtual address
4 size of raw data
1F2 file pointer to raw data (000001F2 to 000001F5)
1F6 file pointer to relocation table
0 file pointer to line numbers
1 number of relocations
0 number of line numbers
40300040 flags
Initialized Data
4 byte align
Read Only
RAW DATA #6
00000000: 00 00 00 00 ....
RELOCATIONS #6
Symbol Symbol
Offset Type Applied To Index Name
-------- ---------------- ----------------- -------- -------
00000000 DIR32 00000000 C ??__Egi@@YAXXZ (void __cdecl `dynamic initializer for 'gi''(void))
CRT 定义两个指针:
.CRT$XCA
中的__xc_a
.CRT$XCZ
中的__xc_z
除 __xc_a
和 __xc_z
之外,两个组都没有定义任何其他符号。
现在,链接器在读取各个 .CRT
子节($
后面的节)时,会将这些子节组合在一个节中,并按字母顺序排列。 这表示用户定义的全局初始值设定项(Microsoft C++ 编译器将其置于 .CRT$XCU
中)始终出现在 .CRT$XCA
之后和 .CRT$XCZ
之前。
本部分应当与以下示例类似:
.CRT$XCA
__xc_a
.CRT$XCU
Pointer to Global Initializer 1
Pointer to Global Initializer 2
.CRT$XCZ
__xc_z
CRT 库会同时使用 __xc_a
和 __xc_z
来确定全局初始值设定项列表的开头和结尾,原因在于这些设定项在图像加载后在内存中布局的方式。
用于初始化的链接器功能
C++ 标准版不提供符合标准的方法,用于为用户提供的全局初始值设定项指定跨翻译单元的相对顺序。 但是,Microsoft 链接器按字母顺序对 .CRT
子节进行排序,因此可以利用此排序来指定初始化顺序。 我们不建议使用此 Microsoft 特定技术,并且可能不会在未来版本中提供。 我们对此进行记录只是为了防止你创建的代码发生中断且方式难以诊断。
为了帮助防止代码出现问题,从 Visual Studio 2019 版本 16.11 开始,我们默认添加了两个新警告:C5247 和 C5248。 启用这些警告以检测创建自己的初始值设定项时出现的问题。
可以将初始值设定项添加到未使用的保留节名称,按照编译器生成动态初始值设定项的特定相对顺序创建这些初始值设定项:
#pragma section(".CRT$XCT", read)
// 'i1' is guaranteed to be called before any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCT")) type i1 = f;
#pragma section(".CRT$XCV", read)
// 'i2' is guaranteed to be called after any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCV")) type i2 = f;
现在编译器或 CRT 库未使用名称 .CRT$XCT
和 .CRT$XCV
,但不能保证以后一直不使用。 而且,你的变量仍可由编译器进行优化。 在采用此技术之前,请考虑潜在的工程、维护和可移植性问题。