次のページ 前のページ 目次へ

7. PEフォーマットからELFフォーマットへの変換方法

Winuxでは6.3で述べた通り、Windows環境で作成したPEフォーマットを独自の変換プログラムにより、ELFフォーマットに変換して、Linux上で実行させる方法を取ります。 従って、変換プログラム自体がWinuxライブラリの中枢部という事ができます。

7.1 プロセス実行のしくみを理解する

WindowsでもLinuxにおいても実行形式ファイルのメモリへのローディングと実行開始方法は大差ありません。 OSは実行が指示されると、そのプロセス独自の仮想メモリ空間を作成します。 アドレス空間は32ビット(4Gバイト)で、独立した空間であるために、他のプロセスと同じアドレスを使用しても全然問題ありません。 アドレス空間が作成されると、物理メモリを割り当てながら実行形式ファイルの内容を仮想メモリ空間にローディングします。 実行形式ファイルには仮想メモリ空間にローディングする為の情報(開始アドレス,バイト数など)が入っており、OSはこの情報に従ってローディングを行います。 通常は命令空間,データ空間,読み出し専用データ空間,リンク情報空間など、複数のブロックに分かれています。 ローディングが終われば、スタックなどの初期化を行い、エントリポイントという実行を開始する仮想アドレスからプロセスを実行します。

7.2 変換プログラムの基本的な動作

実行形式ファイルのメモリにローディングするイメージは複数のブロックに分かれていますが、 コード(命令)はリロケータブルなアドレッシング(相対的なアドレス指定))を用いていないため、 各ブロックの指定された仮想アドレス通りに配置しないと動作しません。 ですから、基本的にブロック(セクション)の構成を変更してはなりません。 よって、PEフォーマットで定義されているセクションをそのままELFフォーマットのセクションとします。 つまり、データイメージはそのままコピーして使用し、ヘッダ部のフォーマットのみを変換します。 ただし、リンク部においては使用できない為に変換しません。 それと、エントリポイントも変更しなければなりません。 エントリポイントが同一だと、Linuxで実行する時にWindowsのスタートアップルーチンが実行されてしまい、セグメンテーションフォールトが発生してしまいます。

7.3 セクション情報の変換

・ELFヘッダ
LinuxではELFヘッダはコード(命令)セクションに入れられ、メモリに配置されるようです。 つまりコードセクションの先頭にはELFヘッダが配置されています。Winuxにおいてもそれに従いました。

・配置仮想アドレス
仮想アドレスはPEの場合、ベースアドレス+オフセットという記述になっています。ELFでは絶対アドレスなのでベースアドレスを加算して格納します。 つまり、ELFセクションヘッダ.p_vaddr = PEセクションヘッダ.VirtualAddress + PEオプショナルヘッダ.ImageBaseとなります。

・メモリサイズ
メモリサイズについては変更できませんので、そのままコピーします。 ELFセクションヘッダ.p_memsz = PEセクションヘッダ.Misc.VirtualSize となります。

・ファイルサイズ
ファイルサイズは通常メモリサイズと同じですが、メモリサイズよりも小さいものがあります。 これは初期化しないデータ領域の分ファイルサイズを小さくするためのものですが、 私のLinux環境ではうまくいかなかったので、ファイルサイズとメモリサイズを同一にしました。 この為、ELFに変換した場合ときに若干ファイルサイズが大きくなります。

7.4 エントリポインタの変換

エントリポインタをそのままコピーすることはできません。 Windowsのスタートアップルーチンが呼ばれる為です。 このルーチンはWindowsシステムコールを呼ぶ為にLinuxではセグメンテーションフォールトになります。

そこで、なんとかしてメインルーチンの仮想アドレスを探さなければなりません。 これには、昔ながらのデバッグテクニックを使います。 MS−DOSが出た頃のアセンブラ言語のデバッグで、ある関数の先頭をサーチする場合、 あらかじめコード部に文字列を書き込んでおき、ジャンプ命令で文字列データを飛び越すようにコーディングします。 デバッグを行うときはメモリ内を書き込んだ文字列で検索して、その関数のアドレスを見つける方法です。 コード部のメモリイメージ(機械語)が文字列と一致することは無いに等しい確率なので、ほぼ100%検索は成功します。

具体的なコーディングは以下のようになります。

BOOL	bAlwaysFalse = FALSE;
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,LPSTR pcCommandLine,INT iShow)
{
	// TODO:ここに Windows での前処理を書く

	if( bAlwaysFalse ){ 
		// if( FALSE ) とすると最適化によりカットされるのでコンパイラを騙す為にこのような表現になっている。 
		// Linux 時にエントリポイントを変更する為のパターン(0xF4を8個連続させる)。実行されるものではない。
		_asm{
					hlt;
					hlt;
					hlt;
					hlt;
					hlt;
					hlt;
					hlt;
					hlt;
		}

		// TODO:ここに Linux での前処理を書く

	}

	// ここから下は Windows,Linux 共通の処理
	// TODO:ここでプロセスのメイン関数を呼び出す
}
上記の様にコーディングすると、コードデータ中に0xF4が8個連続する部分を検出することができます。 その部分の直後のアドレス(Linuxでの前処理部分の先頭になる)をエントリポイントとして設定します。

ただ、これはあくまで手品の種明かしであって、それを作品まで仕上げる努力とそれを実行する手品師が必要なのは言うまでもありません。 確かにエントリポインタを設定すればOSはそこにプログラムカウンタを設定し実行を開始してくれますが、 その状況は高級言語で書かれたプログラムを実行できるものではないということです。 私が認識している主なものを挙げると、
1)高級言語で書かれた関数を実行するためのスタックエリアが足らないので、自分で領域を確保しスタックポインタを設定しなければならない。
2)Linux では WinMain 関数を終了するまでにプログラム終了のシステムコールを呼び出す必要がある。
3)Linux での前処理部では関数として呼ばれていないのでローカル変数は使用してはならない。
という問題がありますが1度作ってしまえば終わりということで、気合いをいれてコーディングしました。


次のページ 前のページ 目次へ