Я наконец-то понял, как это сделать в 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
таким образом, только ваш код, который Я не уверен, что произойдет, если вы свяжетесь со стандартным кодом «С», в этом случае вам, возможно, придется поместить его в блок .CRT$XIT
).
Одна вещь, которую я обнаружил, заключалась в том, что «пре-терминаторы» и «терминаторы» на самом деле не сохраняются в исполняемом файле, если вы ссылаетесь на версии DLL библиотеки времени выполнения. Из-за этого вы не можете использовать их в качестве общего решения. Вместо этого, способ, которым я выполнял свою конкретную функцию в качестве последней пользовательской функции, заключался в простом вызове atexit()
в инициализаторах C, таким образом, никакая другая функция не могла быть добавлена в стек (который будет вызываться в обратном порядке, к которому добавляются функции и как все глобальные / статические деконструкторы вызываются).
Только одна заключительная (очевидная) заметка, написанная с учетом библиотеки времени выполнения Microsoft. Он может работать аналогично на других платформах / компиляторах (надеюсь, вам удастся просто изменить имена сегментов на те, которые они используют, ЕСЛИ они используют ту же схему), но не рассчитывайте на это.