我终于想出了如何在 Windows/Visual Studio 下做到这一点。再次查看 crt 启动函数(特别是它调用全局初始化器的位置),我注意到它只是运行包含在某些段之间的“函数指针”。因此,只要对链接器的工作原理有一点了解,我就想到了这个:
#include <iostream>
using std::cout;
using std::endl;
// Typedef for the function pointer
typedef void (*_PVFV)(void);
// Our various functions/classes that are going to log the application startup/exit
struct TestClass
{
int m_instanceID;
TestClass(int instanceID) : m_instanceID(instanceID) { cout << " Creating TestClass: " << m_instanceID << endl; }
~TestClass() {cout << " Destroying TestClass: " << m_instanceID << endl; }
};
static int InitInt(const char *ptr) { cout << " Initializing Variable: " << ptr << endl; return 42; }
static void LastOnExitFunc() { puts("Called " __FUNCTION__ "();"); }
static void CInit() { puts("Called " __FUNCTION__ "();"); atexit(&LastOnExitFunc); }
static void CppInit() { puts("Called " __FUNCTION__ "();"); }
// our variables to be intialized
extern "C" { static int testCVar1 = InitInt("testCVar1"); }
static TestClass testClassInstance1(1);
static int testCppVar1 = InitInt("testCppVar1");
// Define where our segment names
#define SEGMENT_C_INIT ".CRT$XIM"
#define SEGMENT_CPP_INIT ".CRT$XCM"
// Build our various function tables and insert them into the correct segments.
#pragma data_seg(SEGMENT_C_INIT)
#pragma data_seg(SEGMENT_CPP_INIT)
#pragma data_seg() // Switch back to the default segment
// Call create our call function pointer arrays and place them in the segments created above
#define SEG_ALLOCATE(SEGMENT) __declspec(allocate(SEGMENT))
SEG_ALLOCATE(SEGMENT_C_INIT) _PVFV c_init_funcs[] = { &CInit };
SEG_ALLOCATE(SEGMENT_CPP_INIT) _PVFV cpp_init_funcs[] = { &CppInit };
// Some more variables just to show that declaration order isn't affecting anything
extern "C" { static int testCVar2 = InitInt("testCVar2"); }
static TestClass testClassInstance2(2);
static int testCppVar2 = InitInt("testCppVar2");
// Main function which prints itself just so we can see where the app actually enters
void main()
{
cout << " Entered Main()!" << endl;
}
哪个输出:
Called CInit();
Called CppInit();
Initializing Variable: testCVar1
Creating TestClass: 1
Initializing Variable: testCppVar1
Initializing Variable: testCVar2
Creating TestClass: 2
Initializing Variable: testCppVar2
Entered Main()!
Destroying TestClass: 2
Destroying TestClass: 1
Called LastOnExitFunc();
这是由于 MS 编写其运行时库的方式。基本上,他们在数据段中设置了以下变量:
(虽然此信息是版权信息,但我认为这是合理使用,因为它不会贬低原件,仅供参考)
extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[];
extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[]; /* C initializers */
extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[];
extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[]; /* C++ initializers */
extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[];
extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[]; /* C pre-terminators */
extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[];
extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[]; /* C terminators */
在初始化时,程序简单地从 '__xN_a' 迭代到 '__xN_z'(其中 N 是 {i,c,p,t})并调用它找到的任何非空指针。如果我们只是在段 '.CRT$XnA' 和 '.CRT$XnZ' 之间插入我们自己的段(其中 n 再次是 {I,C,P,T}),它将与其他所有内容一起调用通常会被调用。
链接器只是按字母顺序连接段。这使得选择何时调用我们的函数变得非常简单。如果您查看defsects.inc(在$(VS_DIR)\VC\crt\src\ 下找到),您会看到MS 已将所有“用户”初始化函数(即在代码中初始化全局变量的函数)放在以“U”结尾的段中.这意味着我们只需要将初始化程序放在早于 'U' 的段中,它们将在任何其他初始化程序之前被调用。
您必须非常小心,不要使用在您选择的函数指针放置之后才初始化的任何功能(坦率地说,我建议您只使用 .CRT$XCT 这样它只有您的代码没有已初始化。我不确定如果您使用标准的“C”代码链接会发生什么,在这种情况下您可能必须将其放在 .CRT$XIT 块中)。
我确实发现的一件事是,如果您链接到运行时库的 DLL 版本,“预终止符”和“终止符”实际上并没有存储在可执行文件中。因此,您不能真正将它们用作通用解决方案。相反,我让它作为最后一个“用户”函数运行我的特定函数的方式是在“C 初始化程序”中简单地调用atexit(),这样,就不会将其他函数添加到堆栈中(将被调用以相反的顺序添加函数,以及调用全局/静态解构器的方式)。
最后一个(显而易见的)注释,这是在考虑 Microsoft 的运行时库的情况下编写的。它在其他平台/编译器上的工作方式可能类似(希望您只需将段名称更改为他们使用的任何名称,如果他们使用相同的方案),但不要指望它。