CRT の初期化

この記事では、CRT がネイティブ コードのグローバル状態を初期化する方法について説明します。

既定では、リンカーには、独自のスタートアップ コードを提供する CRT ライブラリが含まれています。 このスタートアップ コードは、CRT ライブラリを初期化し、グローバル初期化子を呼び出し、コンソール アプリケーション用のユーザー指定の main 関数を呼び出します。

Microsoft 固有のリンカー動作を利用して、独自のグローバル初期化子を特定の順序で挿入することも可能ですが、推奨はされません。 このコードは移植可能ではなく、いくつかの重要な注意事項があります。

グローバル オブジェクトの初期化

次の C++ コードについて考えてみましょう (定数式の関数呼び出しが許可されないため、C ではこのコードは許可されません)。

int func(void)
{
    return 3;
}

int gi = func();

int main()
{
    return gi;
}

C と C++ の標準に従えば、func()main() を実行する前に呼び出す必要があります。 では、呼び出し元は何でしょうか。

呼び出し元を決定する 1 つの方法は、func() 内にブレークポイントを設定し、アプリケーションをデバッグし、スタックを調査することです。 これは Visual Studio に CRT ソース コードが含まれているから可能なことです。

スタック上の関数を参照すると、CRT が関数ポインターのリストを呼び出していることがわかります。 これらの関数は、func()、またはクラス インスタンスのコンストラクターに似ています。

CRT では Microsoft C++ コンパイラから関数ポインターのリストが取得されます。 コンパイラがグローバル初期化子を確認すると、.CRT$XCU セクション内に動的初期化子を生成します (CRT はセクション名、XCU はグループ名です)。 動的初期化子を取得するには、dumpbin /all main.obj コマンドを実行し、.CRT$XCU セクションを検索します。 このコマンドは、main.cpp が C ファイルではなく C++ ファイルとしてコンパイルされる場合にのみ適用されます。 次の例のようになります。

SECTION HEADER #6
.CRT$XCU name
       0 physical address
       0 virtual address
       4 size of raw data
     1F2 file pointer to raw data (000001F2 to 000001F5)
     1F6 file pointer to relocation table
       0 file pointer to line numbers
       1 number of relocations
       0 number of line numbers
40300040 flags
         Initialized Data
         4 byte align
         Read Only

RAW DATA #6
  00000000: 00 00 00 00                                      ....

RELOCATIONS #6
                                               Symbol    Symbol
Offset    Type              Applied To         Index     Name
--------  ----------------  -----------------  --------  -------
00000000  DIR32             00000000           C         ??__Egi@@YAXXZ (void __cdecl `dynamic initializer for 'gi''(void))

CRT では、2 つのポインターを定義します。

  • .CRT$XCA』の「__xc_a
  • .CRT$XCZ』の「__xc_z

両グループとも、__xc_a__xc_z を除いては、定義された他のシンボルはありません。

リンカーがさまざまな .CRT サブセクション ($ の後の部分) を読み取るとき、それらを 1 つのセクションに結合し、アルファベット順に並べ替えます。 つまり、ユーザー定義のグローバル初期化子 (Microsoft C++ コンパイラでは .CRT$XCU 内にあります) は常に .CRT$XCA の後にあり、.CRT$XCZ の前にあります。

セクションは次の例のようになります。

.CRT$XCA
            __xc_a
.CRT$XCU
            Pointer to Global Initializer 1
            Pointer to Global Initializer 2
.CRT$XCZ
            __xc_z

CRT ライブラリでは、イメージの読み込み後にメモリに配置される方法により、グローバル初期化子リストの開始と終了の両方__xc_a__xc_zを使用して決定します。

初期化のためのリンカー機能

C++ 標準では、ユーザー指定のグローバル初期化子について、変換単位間での相対的順序を指定するための準拠した方法が提供されていません。 ただし、Microsoft リンカーでは .CRT サブセクションがアルファベット順に並べ替えられるので、この順序を利用して初期化の順序を指定することは可能です。 この Microsoft 固有の手法はお勧めはできません。将来のリリースで機能しなくなる可能性があります。 ここで説明していることは、あくまでも、作成したコードが機能しなくなった場合に、診断すら困難な状況になるのを回避するのが目的です。

Visual Studio 2019 バージョン16.11 以降では、コードの問題を回避するために、C5247 および C5248 という 2 つの新しい警告が既定で追加されました。 独自の初期化子を作成する際には、これらの警告を有効にして問題を検出してください。

未使用の予約済みセクション名に初期化子を追加することで、コンパイラによって生成される動的初期化子に対し、特定の相対的順序でそれらを作成することができます。

#pragma section(".CRT$XCT", read)
// 'i1' is guaranteed to be called before any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCT")) type i1 = f;

#pragma section(".CRT$XCV", read)
// 'i2' is guaranteed to be called after any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCV")) type i2 = f;

.CRT$XCT および .CRT$XCV という名前は、コンパイラや CRT ライブラリでは現在使用されていませんが、今後も使用されないという保証はありません。 また、コンパイラによって変数が最適化される可能性もあります。 この手法を採用する前に、エンジニアリング、保守、および移植性に関する潜在的な問題について検討してください。

関連項目

_initterm, _initterm_e
C ランタイム (CRT) と C++ 標準ライブラリ (STL) .lib ファイル