我终于想出了如何在 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 版本,“预终止符”和“终止符”实际上并没有存储在可执行文件中。因此,您不能真正将它们用作通用解决方案。相反,我让它作为最后一个“用户”函数运行我的特定函数的方式是atexit()
在“C 初始化程序”中简单地调用,这样,就不能将其他函数添加到堆栈中(这将被反向调用添加函数的顺序以及调用全局/静态解构函数的方式)。
Just one final (obvious) note, this is written with Microsoft's runtime library in mind. It may work similar on other platforms/compilers (hopefully you'll be able to get away with just changing the segment names to whatever they use, IF they use the same scheme) but don't count on it.