トップ 検索 一覧 差分 ソース ヘルプ RSS ログイン

mainとスレッドの前後処理(1)

この記事は連続記事の先頭です。次の記事はmainとスレッドの前後処理(2)です。

この記事は早くも古いものとなりました。詳細はMinGW+pthread2010.03?を参照。

mainとスレッドの前後処理(1)

C/C++でmainの開始前・終了後に処理をしたいことがある。また、スレッドの開始前・終了後にも処理をしたいことがある。要はプロセス・スレッドの生成・消滅に対するフック処理なのだが、主にMinGW、そしておまけでVisual C++でこれらを行うためのメモ。当然Windows環境がターゲットだが、知識としてGCC全般(というかELF)に関わる部分も扱う。

調査の結果、話が広がりすぎて上手くまとめられる自信がないのだけれど。日本語圏ではこれに関する記事が少ないようなので、メモレベルでも頑張って書いてみる。なお、この記事からのリンク先は英語のサイトが殆どなので注意。

 調査の動機・経緯

Windows上でPOSIX互換のスレッド機能(pthread)を実現するための、POSIX Threads (pthreads) for Win32(以下pthread-w32)という有名なライブラリがある。

しかしこのライブラリはバージョン2.8.0現在、staticリンクで使用するためには、使用する側のソースコードに細工が要る。詳細はMinGW+ffmpeg(5)pthreadMinGW+ffmpeg(A)pthreadは複雑を見てもらうとして、プロセス・スレッドの開始時・終了時にそれぞれ、特定の関数を呼ぶ必要がある。つまり、POSIXのpthreadだけを想定して書かれているプログラムは、そのままpthread-w32をリンクするだけでは実行時にクラッシュする。

このような仕様ではPOSIX互換とは言い難く、また、他人様の書いたコードを単にビルドして使用したい人間にとっては、毎回コード修正が必要というのは好ましい状況ではない。

猫科研究所では主にffmpeg, xvidcore, x264で対応状況がまちまちなことに困っていた。が、先日、pthreads-win32 updated - IMPORTANT ≪ autobuilds logという記事を見つけた。なんと、この制限を上手く取り払っているらしい。

これがどう実現されているのか、という興味から今回は調査を始めた。なお、コードを見ればある程度その仕組みは想像が着くのだが、ここでは理論的に追って行く。

問題の焦点

結論から言ってしまうと問題であるのはスレッドの終了時なのだが、一応全て追って行こう。

猫科研究所では、pthread-w32でこの制限を撤廃するのは難しいだろうと予測していた。DLLではDllMain()関数でプロセス・スレッドの開始・終了を検知できるのは有名な話だが、スタティックリンクされるライブラリ側でそれが可能であるとは思えなかった。

しかし、言われてみればその通りなのだが、プロセスに関しては標準C++の仕様の範囲内でも、この仕組は必要なのであった。

 プロセスの開始・終了

以下のコードを見て欲しい。

#include <iostream>

using namespace std;

class Foo {
public:
	Foo(){
		cout << "constructor!" << endl;
	};
	~Foo(){
		cout << "destructor!" << endl;
	};
	void bar(){
		cout << "main!" << endl;
	}
};

Foo foo;

int main(int argc, char* argv[]){
	foo.bar();
	return 0;
}

これの実行結果(g++ 4.4.1 TDM-2)は以下の通り。

$ ctordtor.exe
constructor!
main!
destructor!

そう、グローバルまたはスタティックなインスタンスのために、コンストラクタ・デストラクタはmainの外で呼ばれる。これはstaticリンクされるライブラリでも例外ではない。そしてDLLではDllMainの前にDllMainCRTStartupが実行されるのは知っている。

標準C/C++ではこれらの実装方法を規定しているわけではないが、そういう仕組自体は処理系に必ず必要ということだ。そこに割り込む方法があるかもしれないし、最悪、クラス化してC++のグローバルorスタティック変数にしてしまえばなんとかなる。なお、プログラムが途中でexitしても当然デストラクタは実行されるし、インスタンスが1度も使用されずともコンストラクタ・デストラクタは実行される。例えば上記の例で以下のようにしても"constructor!"と"destructor!"は表示される。

int main(int argc, char* argv[]){
    // foo.bar();
    exit(0);
    cout << "this is never used." << endl;
    return 0;
}

プロセスの開始・終了時処理の実装

さて、言語レベルから下り、実装レベルの仕組みを見ると、Windowsではこれらを実行ファイル(PE)の特定のセクションを使用して実現している。セクションと言えば.textや.rdataなどが有名だが、普通のプログラミングをしている限り、それほど気に掛けることはない。組み込み系プログラミングやセキュリティ関連の若干深い部分では重要だが、一般的にはコンパイラ・リンカが自動的に適切に扱ってくれるからだ。

セクション自体の説明は長くなるので省くが、誤解を恐れずに言えば名前のついたメモリ領域だと思えばいい。そのメモリ領域の名前によって、特殊な処理が行われるようになっている。ここで期待する処理とは、特定のセクション内のデータを関数へのポインタとみなし、決まったタイミングでそれらの関数を実行してくれることだ。

この機能自体はOS(というか実行ファイルの形式)固有のものなので、コンパイラ・リンカによってその記述・指定方法は異なっても、最終的にはこのセクションの機能に到達する。この記事ではこれを初期化セクション(終了セクション)と呼ぶ事にする。

初期化・終了セクションに関する基本的なルールは以下の通り。

  • セクション名は".CRT$X??"(「?」には任意のアルファベット)にする。
  • つまり".CRT$XAA"〜".CRT$XZZ"あたりの名前が使用される。
  • セクション名には大文字・小文字の区別がない。
  • 正確には"$"の前後で".CRT"をセクション名、"X??"をグループ名と言うらしい。
  • アルファベットの若い順にソートされて格納される。
  • 最後の文字がAとZのセクションはマーカー(目印)として使用されるので、関数は登録せず0で埋める。
  • ".CRT"に属するセクションは最終的に".data"に統合される。

これに則った上で、さらに名前付のルール(というよりは慣行?)があり、Running Code Before and After Main CodeGuru.comによると、2文字目によって以下のように分類されている。

  • ".CRT$XI?"はCの初期化。
  • ".CRT$XC?"はC++の初期化。
  • ".CRT$XP?"は終了前処理(C++用?)。
  • ".CRT$XT?"は終了時処理。

また、about #pragma init_seg(compiler)によれば、このうち".CRT$XC?"の一部に関して、Visual C++では以下の#pragmaで簡易的に定義できる。

  • ".CRT$XCC":#pragma init_seg(compiler)
    • コンパイラとCランタイムライブラリ用の初期化セクション。
  • ".CRT$XCL":#pragma init_seg(lib)
    • サードパーティ製のライブラリ用の初期化セクション。
  • ".CRT$XCU":#pragma init_seg(user)
    • ユーザ用の初期化セクション。

init_segディレクティブは#pragma init_seg("mysegment")のように任意のセクション名を指定する事もできる。詳しくはMSDNのinit_segを参照。

これらの情報を総合すると、2文字目は言語処理系によって、3文字目は処理の深度によってルール付けされている。そしてどうやらVisual C++ではユーザが初期化セクションを定義する場合、#pragma init_seg(user)を使って欲しいようだ。ただしこの記事の経緯であるpthread-w32はライブラリなので#pragma init_seg(lib)でもいいかもしれない。

セクションの定義にはこの他、#pragmaディレクティブのsectiondata_segを使う方法もある。

なお、GCCでは

 __attribute__ ((section("セクション名")))

で特定のセクションへの配置ができる。GCCやLinuxの世界でメジャーなELFフォーマットもWindowsのCOFF・PEと同じような決まり(仕組み)があり、セクション名は.ctors(constructorsの意味)と.dtors(destructorsの意味)になる。定義の書式やセクション名が違うだけで、ルールはVisual C++の場合と同じようなものだ。こちらの方が単純で分かりやすい気もする。

さて、あとはスレッドの開始・終了だが、こちらは厄介だ。


この記事は連続記事の先頭です。次の記事はmainとスレッドの前後処理(2)です。

最終更新時間:2010年02月14日 04時55分13秒