EternalWindows
GINA / コンテキスト

WlxNegotiateがTRUEを返した後、その次にはWlxInitializeが呼ばれます。 この関数で行う主な作業は、GINAで扱う変数やデータの初期化となります。

BOOL WlxInitialize(
  LPWSTR lpWinsta,
  HANDLE hWlx,
  PVOID pvReserved,
  PVOID pWinlogonFunctions,
  PVOID *pWlxContext
);

lpWinstaは、ウインドウステーションの名前を格納した文字列のアドレスです。 hWlxは、Winlogonのハンドルです。 これは、ディスパッチテーブルの関数を呼ぶ際に必要となります。 pvReservedは、予約されています。 pWinlogonFunctionsは、ディスパッチテーブルのアドレスです。 pWlxContextは、コンテキスト変数のアドレスを指定します。 ここで指定したアドレスは、後続のエクスポート関数の第1引数に渡ります。 戻り値は、TRUEならば関数が成功となり後続の関数が呼ばれ、 FALSEを返した場合、WlxNegotiateと同じダイアログが表示され再起動することになります。

pWlxContextは、開発者のために用意された引数と考えることができます。 この変数に指定したアドレスは、後続のエクスポート関数に指定されることになるため、 データをグローバルに宣言せずに他の関数から参照することができます。 しかし、単純に一個の変数のアドレスを指定するだけでは、 その変数の値しか後続の関数に送れないことになるため、 必要な変数をメンバとして持つ構造体のアドレスを指定すべきです。

struct GINACONTEXT {
	HANDLE                    hWlx;
	HANDLE                    hToken;
	HINSTANCE                 hinstDll;
	PWLX_MPR_NOTIFY_INFO      pMprNotifyInfo;
	PWLX_DISPATCH_VERSION_1_0 pDispatchTable;
};

hWlxとpDispatchTableは、ディスパッチテーブルの関数を呼び出すために必要ですから、 この2つの変数は必ずコンテキストに含めるべきです。 hTokenとpMprNotifyInfoは必要になったときに説明しますが、 hinstDllについては無理にメンバ変数として加える必要はありません。 このメンバ変数は、ディスパッチテーブルのWlxDialogBoxParamの呼び出しを想定したもので、 第2引数にはダイアログリソースを格納したDLLのアドレスを指定することになっています。 つまり、Winlogonのアドレス空間におけるGINAのアドレスを指定します。 このアドレスは、たとえば次のように取得することが可能です。

lpgc->hinstDll = GetModuleHandle(L"gina.dll");

このコードは、GINAのファイル名が一定ではないという問題を助長しているといえます。 以前説明したように、WinlogonがGINAをロードしているときには、 そのロードされているGINAと同じファイル名を持つ新しいGINAをsystem32フォルダに コピーできませんから、ファイル名は随時変更していくことになります。 これは、つまり上記コードのgina.dllの部分も変更するということを意味していますから、 コードの運用の面を考えると非効率の感は拭えないところでしょう。 DLLのエントリポイントされるDllMainでは、第1引数にDLLのアドレスが格納されているため、 この関数内でアドレスを取得するのがやはり確実といえるはずです。

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpReserved)
{
	switch (dwReason) {
	
	case DLL_PROCESS_ATTACH:
		g_hinstDll = hinstDll;
		DisableThreadLibraryCalls(hinstDll);
		return TRUE;
	}

	return TRUE;
}

g_hinstはグローバル変数であり、DllMainの第1引数を受け取ります。 これにより、WlxInitializeではこの変数を基にhinstDllメンバを初期化できます。 しかし、既にGINAのアドレスはg_hinstとしてグローバルに宣言されていますから、 hinstDllメンバを利用するかは好みの問題といえるでしょう。 また、DisableThreadLibraryCallsもスレッド通知が不要という僅かな パフォーマンスの解消を狙っているだけで、特別必須な処理というわけでもありません。 次に、WlxInitializeの初期化例を示します。

BOOL WINAPI WlxInitialize(LPWSTR lpWinsta, HANDLE hWlx, PVOID pvReserved, PVOID pWinlogonFunctions, PVOID *pWlxContext)
{
	LPGINACONTEXT lpgc;

	lpgc = (LPGINACONTEXT)LocalAlloc(LPTR, sizeof(GINACONTEXT));
	
	lpgc->hWlx           = hWlx;
	lpgc->hinstDll       = g_hinstDll;
	lpgc->pDispatchTable = (PWLX_DISPATCH_VERSION_1_0)pWinlogonFunctions;

	*pWlxContext = (LPVOID)lpgc;
	
	lpgc->pDispatchTable->WlxUseCtrlAltDel(lpgc->hwlx);

	return TRUE;
}

コンテキストを格納できるだけのメモリを確保し、この時点で初期化できるメンバを初期化します。 ディスパッチテーブルの型はPWLX_DISPATCH_VERSION_1_0となっていますが、 これは、WlxNegotiateでGINAのバージョンをWLX_VERSION_1_0にしたためであって、 常にPWLX_DISPATCH_VERSION_1_0としてよいわけではありません。 pWlxContextにコンテキストのアドレスを格納するにことによって、 後続の関数からコンテキストのメンバを参照することができるようになります。 WlxUseCtrlAltDelの呼び出しについては、次節で説明します。

今回のような関数の実装は、正にGINAが独自の処理を行っているように見受けられますが、 その必要性を疑うように思える関数も、これからいくつか目にしていくだろうと思います。 そのようなとき、その関数の処理だけはスタブに任したいという考えが浮かびますが、 コンテキストの整合性の面を踏まえると、恐らく正しく動作しないことと思われます。 たとえば、スタブがWlxInitializeのpWlxContextを利用する設計である場合、 後続の関数でコンテキストのメンバを変更する可能性というは十分に考えられることです。 スタブとしては個々の関数でコンテキストの内容、及びその他のデータが 正しく更新されているという前提を持っているでしょうから、 オリジナルのGINAが1つでも関数を独自に実装するとなっては、 その関数ではスタブがコンテキストの内容を更新できなくなってしまいます。 つまり、スタブに処理をさせるならば全ての関数を処理の対象としなければならず、 一部の関数だけをスタブに処理させるようなことはあってはならないわけです。 したがって、オリジナルのGINAを開発するならば、全ての関数を独自に実装しなければなりません。

テーマを有効にする方法

GINAを独自に開発するにあたって、デフォルトで得ることのできない機能として、 WindowsXPから登場したテーマがあります。テーマは、WlxLoggedOutSASで ユーザープロファイルがロードされることで有効になりますが、 それ以前の関数においてユーザーインターフェースなどを表示した場合は、 従来のクラシックスタイルの形で表示されてしまいます。 たとえば、WlxLoggedOutSASでログオンダイアログボックスを表示した場合は、 まだユーザーのプロファイルはロードされていませんから、 ログオンダイアログボックスにはテーマが反映されないことになります。 また、たとえユーザーがログオンしたとしても、それが管理者アカウントでない場合は、 依然としてテーマが有効にならないという問題もあります。 テーマを明示的に有効にするには、次のようなコードをWlxInitializeに記述します。

typedef DWORD (WINAPI *LPFNTHEMEWAITFORSERVICEREADY)(DWORD);
typedef BOOL (WINAPI *LPFNTHEMEWATCHFORSTART)(void);

BOOL WINAPI WlxInitialize(LPWSTR lpWinsta, HANDLE hWlx, PVOID pvReserved, PVOID pWinlogonFunctions, PVOID *pWlxContext)
{
	// GINA関連のコード

	if (IsWindowsXP()) {
		HMODULE                      hmod;
		LPFNTHEMEWAITFORSERVICEREADY lpfnThemeWaitForServiceReady;
		LPFNTHEMEWATCHFORSTART       lpfnThemeWatchForStart;

		hmod = LoadLibrary(L"shsvcs.dll");
		if (hmod == NULL)
			return TRUE;
		
		lpfnThemeWaitForServiceReady = (LPFNTHEMEWAITFORSERVICEREADY)GetProcAddress(hmod, (LPSTR)2);
		if (lpfnThemeWaitForServiceReady == NULL) {
			FreeLibrary(hmod);
			return TRUE;
		}

		lpfnThemeWatchForStart = (LPFNTHEMEWATCHFORSTART)GetProcAddress(hmod, (LPSTR)1);
		if (lpfnThemeWatchForStart == NULL) {
			FreeLibrary(hmod);
			return TRUE;
		}

		lpfnThemeWaitForServiceReady(1000);
		lpfnThemeWatchForStart();

		FreeLibrary(hmod);
	}

	return TRUE;
}

shsvcs.dllは、シェルサービスという位置づけのDLLであり、ThemeWaitForServiceReadyと ThemeWatchForStartというテーマに関係する関数をエクスポートしています。 それぞれのプロトタイプは次のようになっています。

EXTERN_C DWORD WINAPI ThemeWaitForServiceReady(DWORD dwTimeout);
EXTERN_C BOOL WINAPI ThemeWatchForStart(void);

ThemeWaitForServiceReadyは、サービスのテーマスタートアップが完了するまで待機し、 完了した場合の戻り値はWAIT_OBJECT_0、タイムアウトの発生によって関数が制御を 返した場合はWAIT_TIMEOUTが返ることになっています。 タイムアウトとして適切な値は、1000ミリ秒から2000ミリ秒の間とされているため、 サンプルコードではdwTimeoutとして1000を指定しています。 もし、ThemeWaitForServiceReadyがWAIT_TIMEOUTを返した場合は、 再度タイムアウト値を指定し、ThemeWaitForServiceReadyを呼ぶべきなのかもしれません。

ThemeWaitForServiceReadyが成功したら、次にThemeWatchForStartを呼び出します。 これにより、ユーザーのログオンによって作成されるプロセス、 及びwinlogonプロセスにてテーマが有効になります。 戻り値としてFALSEが返った場合は、GetLastErrorでエラーコードを取得することができます。 なお、実際にはThemeWaitForServiceReadyかThemeWatchForStartのどちらか一方を 呼び出せば、テーマが有効になっているようにも思えますが、完全を期するために 両方の関数を呼び出すようにしてください。

先のサンプルコードで注意しておくべきところは、関数のアドレスを取得する際に、 関数名ではなく序数を使用している点です。 序数2の関数がThemeWaitForServiceReadyの機能を実装し、 序数1の関数がThemeWatchForStartの機能を実装しています。 名前付きとしてエクスポートされていないことから、 関数名は便宜上の名前ということになりますが、 MicrosoftのKBにそのように記述してあるため、そのまま利用することにしています。 なお、仮にDLLのロードやアドレスの取得に失敗したとしても、 これらの処理がWlxInitializeで行われているということを踏まえて、 戻り値はTRUEを返すようにしてください。



戻る