Я, наконец, выяснил, как это сделать под 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, таким образом, никакая другая функция не могла быть добавлена в стек (который будет вызываться в обратный порядок, к которому добавляются функции, и как все глобальные/статические деконструкторы все называются).
Только одна заключительная (очевидная) заметка, это написано с использованием библиотеки времени исполнения Microsoft. Это может сработать на других платформах/компиляторах (надеюсь, вам удастся просто сменить названия сегментов на все, что они используют, если они используют ту же схему), но не рассчитывают на это.
Вам гарантируется, что конструкция зеркала для разрушения будет создана. – 2009-11-18 01:43:12
Если это так, то, поскольку порядок строительства не гарантируется, вы не можете предсказать порядок зеркального уничтожения. - Doug T. 0 сек назад –
Вы не можете форсировать порядок построения * глобалов в разных единицах перевода *. Решение R.Pate, которое должно помещать все глобалы в одну и ту же блок переводов, гарантирует заказ. –