main が呼ばれる前に実行されるコードのコールスタック

プログラムのエントリーポイントは、main関数ではなく、PE内に定義されたアドレスであることはよく知られているが、それより以前にコード実行できるTLS Callbacksという仕組みはあまり知られていない。

TLS CallbacksはPE内に適切にセクションとディレクトリテーブルを作り、そのテーブルにコールバック関数のアドレスを設定しておくと、イメージローダー(ntdll.dll)によるプロセス初期化処理中に、その関数が実行されるという仕組みである。

この関数の呼び出しをブレークするのは、PEを解析して手動でブレークを置いたり、対応しているデバッガ(OllyDbg 2.0)でブレークをかけたりなどの方法があるが、呼び出しのシーケンスを把握しておいた方が、いざというとき小回りが利くので調べてみた。


調査には、このコードを使用した。このソースには以下の関数呼び出しが含まれている。

これをx64 Vista SP2のWinDbg上で実行させ、コールスタック表示に.ocommand機能を用いた。

結果

この実行ログをまとめると以下のようになる。左が関数の出力で、右にそこに至るまでのコールスタックの要約を示した。OnTlsCallbackがTLS Callbacksによる最も早い呼び出し。Cls::Clsはコンストラクタである。

 1:OnTlsCallback(1)    < ( LdrpCallInitRoutine ) < LdrpCallTlsInitializers < LdrpRunInitializeRoutines < LdrpInitializeProcess < LdrInitializeThunk
 2:OnTlsPrepare        <                             < _initterm_e < _cinit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
 3:Cls::Cls            < dynamic initializer for 'obj' < _initterm < _cinit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
 4:OnProcessInit       <                               < _initterm < _cinit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
 5:main                <                                                    < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
 6:DllMain(1)          < ( LdrpCallInitRoutine ) < LdrpRunInitializeRoutines < ... < LoadLibraryA
 7:DllMain(2)          < ( LdrpCallInitRoutine )                           < LdrpInitializeThread < LdrpInitialize < LdrInitializeThunk
 8:OnTlsCallback(2)    < ( LdrpCallInitRoutine ) < LdrpCallTlsInitializers < LdrpInitializeThread < LdrpInitialize < LdrInitializeThunk
 9:StartRoutine(0)     <                                                  < RtlpTpWorkCallback < TppWorkerThread < BaseThreadInitThunk < RtlUserThreadStart
10:DllMain(2)          < ( LdrpCallInitRoutine )                           < LdrpInitializeThread < LdrpInitialize < LdrInitializeThunk
11:OnTlsCallback(2)    < ( LdrpCallInitRoutine ) < LdrpCallTlsInitializers < LdrpInitializeThread < LdrpInitialize < LdrInitializeThunk
12:StartRoutine(-1)    <                                                                                         < BaseThreadInitThunk < RtlUserThreadStart
13:DllMain(3)          <                                                 < LdrShutdownThread < RtlExitUserThread < BaseThreadInitThunk < RtlUserThreadStart
14:OnTlsCallback(3)    < ( LdrpCallInitRoutine ) < LdrpCallTlsInitializers < LdrShutdownThread < RtlExitUserThread < BaseThreadInitThunk < RtlUserThreadStart
15:DllMain(0)          < ( LdrpCallInitRoutine ) < LdrpUnloadDll < LdrUnloadDll < FreeLibrary
16:Cls::~Cls           < `dynamic atexit destructor for 'obj' < doexit < exit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
17:OnProcessTerm       <                          < _initterm < doexit < exit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart
18:OnTlsCallback(0)    < ( LdrpCallInitRoutine ) < LdrpCallTlsInitializers < LdrShutdownProcess < RtlExitUserProcess < __crtExitProcess < doexit < exit < __tmainCRTStartup < mainCRTStartup < BaseThreadInitThunk < RtlUserThreadStart

結果としてわかったのは、

  • TLS CallbacksはCRT初期化よりも前に呼び出される。
  • C++グローバルインスタンスのコンストラクタはCRT初期化ルーチンの中で呼び出される。
    • よってシンボル名などはコンパイル環境に大きく依存すると思われる。
  • WOW64ではTLS CallbacksおよびDllMainはLdrpCallInitRoutineの同じ個所から呼び出される。
  • x64ネイティブでは、
    • TLS CallbacksはLdrpCallTlsInitializers関数から呼び出される。
    • DllMainは呼び出し理由にごとに違う関数から呼び出される。
  • それ以外はx64/WOW64での大きな違いは見られない。


これらはx86 XP SP3でもWOW64とほぼ同じだった。
以上から、TLS Callbakcs以外は、PEのエントリーポイントを経由しているので、注意深く実行していれば必ず捕捉できることが分かる。ただし、_cinitのようなランタイム関数の中からコードが呼び出されているので、スキップしてしまわないように注意する必要はある。

TLS Callbacksの最初の関数呼び出しは、LdrpCallInitRoutineでブレークすることで監視できるが、この関数はDllMainの呼び出しも担当しているため不要なブレークが多発しそうだ。どちらかというとPEを解析して、コールバック関数自体にブレークを設置した方がよいだろう。

PEを見る

TLS CallbacksはPEにあらかじめ登録されているため、PEを見るツールで場所を特定することができる。実際にツールを使う前に、関連する構造体を説明する。以下は、前述した、TLSを登録するディレクトリテーブルを表すIMAGE_TLS_DIRECTORY構造体である。

// WinNT.h
typedef struct _IMAGE_TLS_DIRECTORY32 {
    DWORD   StartAddressOfRawData;
    DWORD   EndAddressOfRawData;
    DWORD   AddressOfIndex;             // PDWORD
    DWORD   AddressOfCallBacks;         // PIMAGE_TLS_CALLBACK *
    DWORD   SizeOfZeroFill;
    DWORD   Characteristics;
} IMAGE_TLS_DIRECTORY32;

この構造体のAddressOfCallBacksは、TLSコールバック関数の配列の仮想アドレスを指している(ASLRが有効になっている場合は計算が必要)。したがって、このフィールドがポイントする先の値が、コールバック関数のアドレスである。
これを確認したのが下の画像。

実際に登録されているのは、トランポリンコードだった。これはデバッグビルドなせいかもしれない。

おまけ

OllyDbg Version 2.0 - Beta 2 Final になってます。TLS Callbacksでブレークできます。

// TLSディレクトリのアドレスの取得コード

    BYTE* Base = (BYTE*)GetModuleHandle(NULL);
    IMAGE_DOS_HEADER* Dos = (IMAGE_DOS_HEADER*)Base;
    IMAGE_NT_HEADERS* Nt = (IMAGE_NT_HEADERS*)(Base + Dos->e_lfanew);
    IMAGE_DATA_DIRECTORY* DataDirectoryTls = (IMAGE_DATA_DIRECTORY*)&Nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
    IMAGE_TLS_DIRECTORY* TlsDirectory = (IMAGE_TLS_DIRECTORY*)(Base + DataDirectoryTls->VirtualAddress);