CtHelper.exe の解析

全く意味が判らんのだがこの CtHelper.exe は Visual C/C++ のMFC(Microsoft Foundation Class) つまり GUI フレークワークで実装されててイラっとした。 ウィンドウなんか一切必要ないのになんという無駄なことをしているのかまったくもって意味不明である。

Unix 屋に分かりやすいように書くと libc プログラミングで事足りるのになぜか X Toolkit や GTK+/Qt を使ってるようなお話。

Ghidra は C++ の解析があまり得意でないのでクラス使ってるとメンバへのアクセスがすべて先頭ポインタからのオフセットで扱われるから複雑怪奇なコードが生成され可読性が非常に悪い。 これは C の解析でも未知の構造体はすべて先頭からのオフセットになるから同じではあるが、構造体はクラスと違って容易に想像がつくから補完は容易いんだよな。

可読性を上げるために解析結果に C++ クラスの情報を補完していくにもそもそも俺にはそっち方面の知識が全く無く MFC とはなんだと尋ねられたらマクドナルドフライドチキン?なのである。 某 F 方面で MFC ったら Merge From Current だろ(ゲラゲラ)ってやってた事はもう忘れました。

いまさら C++/MFC なんて正気かと思われるかもしれないけど、俺は文系なので古文や漢文そして Ancient Unix のコードを読む努力はするのだ(ただすぐ飽きる)。

Unix C がちょっと書けるだけの俺による令和最新式 C++/MFC プログラミング入門

MFC が流行ってた(流行ってない)頃って俺は Win32 API プログラミングは天才アンダース・ヘルスバーグの産み出した Delphi 使ってたので全く経験が無いのである。 そもそも Visual Studio をインストールした経験も他人のコードのお守りで Visual Basic のコード弄った時くらい。

そもそも Windows って COM/OLE/ActiveX があるから C/C++ なんてドライバ屋でも無きゃ用が無いよね。 大抵の事は Windows Scripting Host そして今は PowerShell で済ませてきてる俺が言うんだから間違いない。 あ、やっぱ Perl Win32 使っていいっすか(死)。

まずは環境構築

MFC 4.2~6.0 のランタイムである mfc42.dll にリンクされてるので Visual C++ 5.0 か 6.0 が開発環境なんだろう。 .NET になる前の 6.0 は鉄板扱いだったけどまだこの令和の世でも 6.0 動かす必要にせまられてインストールCDとプロダクトキーを古売屋から結構な値段買ったりする人いるんすかね。 俺もとある現場で 4.0 を用意しろと言われた時は痺れましたね。

ただの Win32 API プログラミングなら MinGW で用は足りるんだけど、MFC には C++ ABI のせいか不人気だったせいか知らんがオープンソースな実装が無いのである。 よって Visual C/C++ が必要になるのだが Standard 以上でないと付属しないし、当時は若くお金も必要だったので Express しか持ってねえ。

非商用なら無償で先行してた Delphi 6 Personal はほぼ機能制限無しに VCL(Visual Component Library) が使えてたのにね。 まぁ Visual Studio .NET なんて改称したし C/C++ なんか今更使おうとするやつは Hello, World で我慢してろということだったのであろう。

ところが今では Express Edition から Community に名前が改められた後くらいからは MFC も解禁されているのである、過去の遺物だしオプション扱いかつサポート無しだけどな。

もう音楽マシン以外は openSUSE になってるので入れたくないけどこいつに Visual Studio Community 2026 をぶち込む。 Microsoft アカウントや GitHub アカウントとの連携を求めてくるけど必須じゃないのでスキップ、Copilot みたいな著作権ロンダリングに加担したくないし。

コードスケルトンを生成し読んでみる

新しいプロジェクトの作成で MFC アプリを選択すると昔懐かしのウィザードがおっぱじまる。

  • アプリケーションの種類に ダイアログベース
  • ダイアログベースのオプションは <なし>
  • ユーザーインタフェース機能と高度な機能はすべてチェック外す

としておく、なんせ作るのはバックグラウンドで E-MU サウンドデバイスの着脱を監視し PatchMix DSP アプリを起動したり終了させたりするだけのアプリだからな…

これで OK キャンセルボタン付きのダイアログが生成されるのだけどボタンも不要、なんせウィンドウ表示しないからユーザー操作発生しないからね。

そんじゃスケルトン読んでいくか、MFC はフレームワークに任せろー(バリバリ)という時代の遺物で main どころか WinMain すら書かないのである。 どこがエントリポイントになるかというと、ウィザードが生成した CWinApp を継承したクラスの唯一のインスタンスである theApp というグローバル変数に注目しよう。

自動生成されたコードはこんなかんじ。

  • CCtHelper.h
// CCtHelperApp:
// このクラスの実装については、CCtHelper.cpp を参照してください
//

class CCtHelperApp : public CWinApp
{
public:
	CCtHelperApp();

// オーバーライド
public:
	virtual BOOL InitInstance();

// 実装

	DECLARE_MESSAGE_MAP()
};

extern CCtHelperApp theApp;
  • CCtHelper.cpp
// 唯一の CCtHelperApp オブジェクト

CCtHelperApp theApp;

CWinApp の仮想関数 InitInstance がエントリポイントになるのでこいつのオーバーライドが main と現時点では思っておこう。 まぁ先にコンストラクタが呼ばれるけどこれは GCC の __attribute__((constructor)) みたいなもんだと思っておけ。

スケルトンには存在しないが 仮想関数 ExitInstance で終了時処理をオーバライドすることもできる、これは atexit で登録する関数みたいなもんということで。 もちろんデストラクタも実装していいのでそこは __attribute__((destructor)) と考えておくと Unix C がちょっと書けるだけの俺にも理解がはやい。

そんじゃスケルトンの InitInstance の実装を読んでみようか。

  • CCtHelper.cpp
// CCtHelperApp の初期化

BOOL CCtHelperApp::InitInstance()
{
	CWinApp::InitInstance();


	// ダイアログにシェル ツリー ビューまたはシェル リスト ビュー コントロールが
	// 含まれている場合にシェル マネージャーを作成します。
	CShellManager *pShellManager = new CShellManager;

	// MFC コントロールでテーマを有効にするために、"Windows ネイティブ" のビジュアル マネージャーをアクティブ化
	CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));

	// 標準初期化
	// これらの機能を使わずに最終的な実行可能ファイルの
	// サイズを縮小したい場合は、以下から不要な初期化
	// ルーチンを削除してください。
	// 設定が格納されているレジストリ キーを変更します。
	// TODO: 会社名または組織名などの適切な文字列に
	// この文字列を変更してください。
	SetRegistryKey(_T("アプリケーション ウィザードで生成されたローカル アプリケーション"));

	CCtHelperDlg dlg;
	m_pMainWnd = &dlg;
	INT_PTR nResponse = dlg.DoModal();
	if (nResponse == IDOK)
	{
		// TODO: ダイアログが <OK> で消された時のコードを
		//  記述してください。
	}
	else if (nResponse == IDCANCEL)
	{
		// TODO: ダイアログが <OK> で消された時のコードを
		//  記述してください。
	}
	else if (nResponse == -1)
	{
		TRACE(traceAppMsg, 0, "警告: ダイアログの作成に失敗しました。アプリケーションは予期せずに終了します。\n");
		TRACE(traceAppMsg, 0, "警告: ダイアログで MFC コントロールを使用している場合、#define _AFX_NO_MFC_CONTROLS_IN_DIALOGS を指定できません。\n");
	}

	// 上で作成されたシェル マネージャーを削除します。
	if (pShellManager != nullptr)
	{
		delete pShellManager;
	}

#if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS)
	ControlBarCleanUp();
#endif

	// ダイアログは閉じられました。アプリケーションのメッセージ ポンプを開始しないで
	//  アプリケーションを終了するために FALSE を返してください。
	return FALSE;
}

スケルトンのコードではダイアログをモーダルで表示しユーザー入力待ちとなる、ボタンが押された後はそのまま終了である。

コメントにもあるけど main と InitInstance の違いは TRUE を返すと継承元の Run として実装されるイベントループに突入することである。 というかイベントループをメッセージポンプとも呼ぶの知らんかった、Windows はほんま変な言葉だらけやで。

X Toolkit プログラミングでは main などで明示的に XtAppMainLoop を呼び出しイベントループを開始するけど、MFC は隠蔽して後はフレームワークに任せろー(バリバリ)ということ。

こういった誇大広告で隠蔽と重ねそれに迎合する無知蒙昧が世に跋扈すると世界は滅亡に向かってまっしぐらなんだよな、いまやお前の脳を AI に明け渡せである。 フレームワーク大好きな PM(Project Manager) の話であってどっかの国の Prime Minister じゃないよ、あいつが好きなのは frame でなく flaming だろいい加減にしろ!

CtHelper.exe のコードを復元する

そんでいよいよ本題、CtHelper.exe の InitInstance および ExitInstance のコードを Ghidra の解析結果とスケルトンから学んだ MFC のお作法をもって復元する。

class CCtHelperApp : public CWinApp
{
public:
	virtual BOOL InitInstance();
	virtual int ExitInstance();
private:
	CCtHelperDlg *dlg;
	HANDLE sem;
};

LPTSTR ident = _T("CtHelper32");

BOOL
CCtHelperApp::InitInstance()
{
	AfxEnableControlContainer();
	Enable3dControls();

	sem = CreateSemaphore(NULL, 0, 1, ident);
	if (sem != NULL) {
		if (GetLastError() == ERROR_ALREADY_EXISTS)
			return FALSE;
	}
	dlg = new CCtHelperDlg();
	m_pMainWnd = this->dlg;
	if (dlg->Create(IDD_CTHELPER_DIALOG, NULL) == TRUE) {
		dlg->SetWindowText(ident);
		dlg->ShowWindow(SW_HIDE);
	}
	return TRUE;
}

int
CCtHelperApp::ExitInstance() 
{
	if (dlg != NULL) {
		delete dlg;
		dlg = NULL;
	}
	if (sem != NULL) {
		if (CloseHandle(sem) == TRUE)
			sem = NULL;
	}
	return CWinApp::ExitInstance();
}

完全復元する気は無いので Creative 社内で埃かぶってるであろうソースコードとは差異はあると思われるがだいたいこんなん。

どうやら MFC のバージョンが 6.0(mfc42.dll) から 14.0(mfc140.dll) に上がっているせいかスケルトンには無かったコードが存在する。

  1. CWinApp::InitInstance 呼んでない
  2. AfxEnableControlContainer (OLE サポートを有効にする) を呼んでる
  3. Enable3dControls (Windows 95 風のコモンコントロール 3D 化) を呼んでる

1 は MFC 14.0 でも 継承元の CWinApp そして CWinThread にも実装は無いから呼ばなくても副作用はないと思われる。

2 は COM/OLE/ActiveX を有効にしてドラッグアンドドロップなどを有効にするもの、不可視のウィンドウにドラッグアンドドロップってナイスジョーク。

3 は MFC 7.0 の時点で 3D 効果がデフォルトとなり 3,1 以前のスタイルに戻せなくなってる上に API も削除されてるから不要。 つーか Windows 10 からフラット UI 化して Windows 3.1 以前に回帰したから 3D 効果は無効になったが正しいのかもしれない。

そして独自なコード、セマフォ(共有メモリ)によるプロセス間通信で多重起動チェック、すでに CtHelper.exe が起動してたら InitInstance は FALSE を返してアプリケーション終了。 もちろん ExitInstance で解放もする、ReleaseSemaphore 使うんじゃねとツッコミたくなるがカウンタ―使わないからこれでも動くのかね。

そんでスケルトンではダイアログをモーダルで開いて閉じたらアプリケーション終了だから CDialog を継承した CCtHelperDlg クラスのインスタンスはローカル変数としてスタックに置く。 しかし CtHelper.exe はヒープに置いて ExitInstance で解放してるみたい。

これは InitInstance を TRUE で抜けることでイベントループに突入することを意図しこう書いたっぽいのだが、他の部分のコードを読むといろいろつじつまが合わないのだ。 それについてはまた次回以降で CCtHelperDlg の実装を復元する時に話をする。