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}) まで反復し、見つかった null 以外のポインターを呼び出します。セグメント '.CRT$XnA' と '.CRT$XnZ' (ここでも、n は {I,C,P,T} です) の間に独自のセグメントを挿入すると、他のすべてのセグメントと共に呼び出されます。通常は呼び出されます。
リンカは単にセグメントをアルファベット順に結合します。これにより、関数を呼び出すタイミングを非常に簡単に選択できます。defsects.inc
( の下にあります$(VS_DIR)\VC\crt\src\
) を見ると、MS がすべての「ユーザー」初期化関数 (つまり、コード内のグローバルを初期化する関数) を「U」で終わるセグメントに配置していることがわかります。これは、イニシャライザを「U」より前のセグメントに配置するだけでよく、他のイニシャライザの前に呼び出されることを意味します。
関数ポインターの選択した配置が完了するまで、初期化されていない機能を使用しないように十分に注意する必要があります (率直に言って、.CRT$XCT
初期化されていないコードのみをそのように使用することをお勧めします。標準の「C」コードとリンクした場合に何が起こるかわかりません.CRT$XIT
。その場合、ブロックに配置する必要があるかもしれません)。
私が発見したことの 1 つは、ランタイム ライブラリの DLL バージョンに対してリンクすると、「プレターミネーター」と「ターミネーター」が実際には実行可能ファイルに格納されないことです。このため、一般的なソリューションとして実際に使用することはできません。代わりに、最後の「ユーザー」関数として特定の関数を実行する方法は、単にatexit()
「C イニシャライザー」内で呼び出すことでした。この方法では、スタックに他の関数を追加することはできませんでした (逆に呼び出されます)。関数が追加される順序と、グローバル/静的デコンストラクターがすべて呼び出される方法です)。
最後に 1 つだけ (明白な) 注意点として、これは Microsoft のランタイム ライブラリを念頭に置いて書かれています。他のプラットフォーム/コンパイラでも同様に機能する可能性があります(同じスキームを使用している場合は、セグメント名を使用するものに変更するだけで済むことを願っています)が、それを当てにしないでください.