エラー処理のエラー処理

ちょっとした小話。

RtlNtStatusToDosError

RtlNtStatusToDosErrorという関数がある。NTSTATUSをWin32のエラーに変換する関数だ。
こいつをエクスポートしているのはntdll.dll、つまり明示的リンクからのアドレス取得という処理が必要になる。

using _RtlNtStatusToDosError = ULONG(*)(const NTSTATUS Status);
auto hNtdll = GetModuleHandle(L"ntdll.dll");
auto RtlNtStatusToDosError = (_RtlNtStatusToDosError)GetProcAddress(hNtdll, "RtlNtStatusToDosError");
auto err = RtlNtStatusToDosError(STATUS_UNSUCCESSFUL/*=0xC0000001*/);

ところで、RltNtStatusToDosErrorのラッパー関数を作ったとしよう。

ULONG NtStatusToDosError(NTSTATUS Status)
{
	using _RtlNtStatusToDosError = ULONG(*)(NTSTATUS Status);
	auto hNtdll = GetModuleHandle(L"ntdll.dll");
	auto RtlNtStatusToDosError = (_RtlNtStatusToDosError)GetProcAddress(hNtdll, "RtlNtStatusToDosError");
	return RtlNtStatusToDosError(Status);
}
エラー処理のためのエラー処理

上記コードは一見良さそうだが、よくよく考えるとGetModuleHnadleだってGetProcAddressだって失敗することがある。
つまり、この関数自体が失敗したことを想定することもできる。
じゃあ、この関数を改変して、返り値を関数が成功したかどうかにし、GetLastErrorでエラーを取得できるように変更しよう。

BOOL NtStatusToDosError(NTSTATUS Status, ULONG* DosStatus)
{
	using _RtlNtStatusToDosError = ULONG(*)(NTSTATUS Status);
	auto hNtdll = GetModuleHandle(L"ntdll.dll");
	if (hNtdll == NULL)
	{
		SetLastError(GetLastError()); //わざわざこんなことをする必要はないがわかりやすさのため
		return FALSE;
	}

	auto RtlNtStatusToDosError = (_RtlNtStatusToDosError)GetProcAddress(hNtdll, "RtlNtStatusToDosError");
	if (RtlNtStatusToDosError == NULL)
	{
		SetLastError(GetLastError());
		return FALSE;
	}

	*DosStatus = RtlNtStatusToDosError(Status);
	return TRUE;
}

これで、失敗したときの備えも出来た。

でも、RtlNtStatusToDosErrorの用途の殆どはエラー処理である。
上記のコードを書いた場合、エラー処理中に別のエラー処理をする必要があることになる。
下手すれば永遠にエラー処理することになるんじゃないか。

余計な心配はしなくていい

とてもとても元も子もないことを言うが、今回のコードに関しては余計なエラー処理はいらないはず。

まず、GetModuleHandle(L"ntdll.dll")についてだが、ntdll.dllはWindowsのプロセスなら必ずロードされる。
そのため、ハンドルが取れないなんてことはないはずだ。
万が一こいつが失敗した場合、LoadLibraryを使ってntdll.dllをロードすればいい。流石にWindowsでntdll.dllが環境に存在しないなんてことはないはずだ。
少なくともXPの時代からあるDLLであるし、Windows10にもある。kernelbase.dllやadvapi32.dllのようにwindowsのバージョンアップに伴い増えたシステムのDLLはあれど、
システムのDLLが減るということはありえないんじゃないか。ましてや、ntdll.dllはシステムコールを含んでる。こんなDLLをWindowsのバージョンアップで消し飛ばすわけがない。

次に、GetProcAddressだが、エクスポート関数を増やすことはあっても消すことはないだろう。
だから、エクスポート関数のアドレスが取れないことはないんじゃないか。

ただ不安があるとすれば、ntdll.dllのエクスポート関数は基本的に非公開関数だということ。
非公開関数は公開関数とは違い、使用するユーザに全責任がかかる。仮に、関数のインターフェースをMicrosoftが勝手に変更してプログラムがクラッシュしてもMicrosoftに非はなくユーザは泣き寝入りすることになる。
(まあ、そのためのInsider previewなのだろう)
RltNtStatusToDosErrorはまだ明確にドキュメント化されている(RtlNtStatusToDosError function (Windows))し、インターフェースもシンプルだから
NtCreateProcessやNtCreateFileなんかよりはインターフェースの変更の可能性が限りなく低い。
GetProcAddressが失敗する心配はないだろう。

結局こんな感じでいい。

ULONG NtStatusToDosError(NTSTATUS Status)
{
	using _RtlNtStatusToDosError = ULONG(*)(NTSTATUS Status);
	auto hNtdll = GetModuleHandle(L"ntdll.dll");
	if (hNtdll == NULL)
	{
		hNtdll = LoadLibrary(L"ntdll.dll");
	}

	auto RtlNtStatusToDosError = (_RtlNtStatusToDosError)GetProcAddress(hNtdll, "RtlNtStatusToDosError");

	return RtlNtStatusToDosError(Status);
}

なによりも普通にコーディングしてて、Rtl~を呼び出すことなんてない。

最後に

勝手に問題を提起して勝手に解決し、挙句の果てに不必要な議論だったことにしてしまった、変な記事になった。
今回やりたかったのは、エラー処理のためにエラー処理が必要になる場面に遭遇した時どうするんだっていう、まあ一種の思考実験なわけだ。
…実際にそんなシチュエーションあるかわからんけど。

こうしてまたこの世から、

この広告は、90日以上更新していないブログに表示しています。

を1つ消し飛ばした。90日間限定だけど。