5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Win32 API] ウィンドウハンドルから実行ファイル名取得と、その注意点

Last updated at Posted at 2021-04-02

WEB 上で調べると EnumProcessModules() 関数を使用した方法がよく出てきますが、実行ファイル名を取得するだけなら不要です。

他にも注意点が複数あるので、まとめました。

0. 要点

  1. モジュールハンドル hModule不要
  2. ファイル名の長さは MAX_PATH_MAX_PATH では不十分
  3. 基本的には、タスクマネージャ等のシステムプロセスを扱えない
  4. 自プロセスしか扱えないファイル名取得関数に注意

1. 全体の流れ

  1. ウィンドウハンドルからプロセス ID を取得し、プロセスをオープン
  2. プロセスから実行ファイル名取得
  3. ファイル名を取得する際、バッファサイズが足りなかった可能性がある場合は、バッファサイズを増やして再取得する (MAX_PATH_MAX_PATH を過信しない)
  4. ファイル名の取得失敗にかかわらず、プロセスクローズ

2. コード (テスト用)

※ここでは文字列をすべて Unicode で扱うことにします。
※ここでは WSL 上の MinGW-w64 でコンパイルすることを想定しています。
※以下のコードでは、タスクマネージャ等のシステムプロセスは扱えません。

main.cpp
#include <windows.h>
#include <iostream>
#include <fcntl.h>

#if ( _WIN32_WINNT < 0x0600 )

// メモ: ここでは、WINVER と _WIN32_WINNT が同じ値に設定されている場合、
//       常に PSAPI_VERSION == 1 が選択されるが、ソースコードを変更した場合にバグが発生しないように記述。
#ifndef PSAPI_VERSION
#if ( WINVER < 0x0601 ) // メモ: 0x0600 は PSAPI_VERSION == 2 対応・非対応が混在している。
#define PSAPI_VERSION 1
#elif
#define PSAPI_VERSION 2
#endif
#endif

#include <psapi.h>

#endif

// 
bool printFileName(HWND hWnd);
bool printFileName(HANDLE hProcess);
bool printFileName(HANDLE hProcess, DWORD nSize);

// 
int wmain() {

	// 出力の文字コード指定
	_setmode(fileno(stdout), _O_U8TEXT);
	_setmode(fileno(stderr), _O_U8TEXT);

	// 
	const HWND hWnd = (HWND) /* 0x00000 */; // TODO: テスト用。手動入力

	if ( ! printFileName(hWnd) ) return -1;

	return 0;

}

bool printFileName(HWND hWnd) {

	// プロセス ID
	DWORD dwProcessId;
	GetWindowThreadProcessId(hWnd, &dwProcessId);

	// プロセスを開く
	// 
	// メモ: これだけでは、タスクマネージャ等のシステムプロセスは扱えない。
#if ( _WIN32_WINNT < 0x0600 )
	const HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId);
#else
	const HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
#endif

	if ( ! hProcess ) {
		std::wcerr << L"OpenProcess Error: " << GetLastError() << std::endl;
		return false;
	}

	// 
	printFileName(hProcess); // ここではエラーにかからわずスルーする (プロセスクローズに注意)

	// プロセスを閉じる
	CloseHandle(hProcess);

	return true;

}

bool printFileName(HANDLE hProcess) {

	// ファイルパス
	// 
	// メモ: 新しい Windows ではファイルパスの長さが _MAX_PATH を超える可能性があるため、
	//       バッファサイズを増やしながら再取得する。
	for (DWORD nSize = 256; nSize <= 32768; nSize <<= 1) {
		if ( printFileName(hProcess, nSize) ) return true;
	}

	std::wcerr << L"Error: printFileNameFrom()" << std::endl;

	return false;

}

bool printFileName(HANDLE hProcess, DWORD nSize) {

	// ファイルパス
	WCHAR lpszFileName[nSize];

#if ( _WIN32_WINNT < 0x0600 )

	const DWORD length = GetModuleFileNameExW(hProcess, NULL, lpszFileName, nSize); // hModule == NULL

    if ( 0 == length ) {
        std::wcerr << L"GetModuleFileNameExW Error: " << GetLastError() << std::endl;
        return true; // ループ終了
    } else if ( nSize - 1 <= length ) {
        return false; // ループ継続
    }

#else

	DWORD length = nSize;

	if ( ! QueryFullProcessImageNameW(hProcess, 0, lpszFileName, &length) ) {
		const DWORD lastError = GetLastError();
		if ( ERROR_INSUFFICIENT_BUFFER == lastError ) {
			return false; // ループ継続
		} else {
			std::wcerr << L"QueryFullProcessImageNameW Error: " << lastError << std::endl;
			return true; // ループ終了
		}
	}

#endif

	// 
	std::wcout << L"File Name: " << lpszFileName << std::endl;

	return true; // ループ終了

}
build.sh
#!/bin/bash

# g++ ver.5.1 以降を想定
x86_64-w64-mingw32-g++ -Wall -std=c++14 \
	-finput-charset=UTF-8 -fexec-charset=CP932 \
	-municode \
	-static-libgcc -static-libstdc++ \
	-DWINVER=0x602 \
	-D_WIN32_WINNT=0x602 \
	-o main.exe \
	main.cpp

./build.sh でビルドできます。

PSAPI のバージョンおよびライブラリのリンクに関して後述
※コンパイルオプションは一例です。
※ここでは簡単のため、シェルスクリプトでビルドすることにします。
※規模が大きいプロジェクトの場合は、何らかのビルドツールを利用した方が良いです。

3. 注意点

3.1. プロセスから実行ファイル名を取得する関数

GetModuleFileNameExW() 関数は Windows XP から利用できますが、Windows Vista から QueryFullProcessImageNameW() 関数でも取得できるようになりました。
Microsoft Docs によると QueryFullProcessImageNameW() の方が「効率的で信頼性がある」と書かれていますが、具体的にどう違うのかは分かりませんでした…。
ですが、新しい Windows で動作させるなら QueryFullProcessImageNameW() の方が良いと思います。

参考「GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs
参考「QueryFullProcessImageNameW function (winbase.h) - Win32 apps | Microsoft Docs

3.2.モジュールハンドル hModule は不要

GetModuleFileNameExW() 関数に関して、Microsoft Docs に「モジュールハンドルが NULL の場合は hProcess の実行ファイルのパスを返す」とあるので、別途モジュールハンドルの取得は不要です。

参考「GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs

QueryFullProcessImageNameW() には元々モジュールハンドルの引数はありません。

3.3. ファイル名の長さについて

最近の Windows ではファイル名の長さが MAX_PATH_MAX_PATH の値を超えることができるため、バッファサイズを固定するのは危ないです。
また、ファイル名の長さを 1 回で確実に取得する方法はありません (バッファのポインタを NULL にしても不可) 。

GetModuleFileNameExW() 関数の場合は、バッファサイズが足りないだけでははエラーを返さないので、文字数が「バッファサイズ - 1」より大きいかどうかを確認します。
その場合には、バッファサイズが足りていない可能性が考えられますので、バッファサイズを増やして再取得します。

QueryFullProcessImageNameW() 関数の場合は、バッファサイズが足りないときはエラーが発生し、ERROR_INSUFFICIENT_BUFFER == GetLastError() になります。そのときも同様にバッファサイズを増やして再取得します。

参考「windows - How can I calculate the complete buffer size for GetModuleFileName? - Stack Overflow
参考「System Error Codes - Win32 apps | Microsoft Docs

3.4. タスクマネージャ等のシステムプロセスについて

上記のコードのままでは、タスクマネージャ等のシステムプロセスに関してオープン時にエラーが発生し、扱うことができません。

SeDebugPrivilege (SE デバッグ特権) を有効にすることで取得できるようになりますが、この特権はもともとデバッグ用のものであり、一般公開するソフトウェアでは使わない方が良いと思います。

どうしてもシステムプロセスを扱いたい場合のみ SeDebugPrivilege (SE デバッグ特権) を有効にして下さい。

参考「OpenProcess function (processthreadsapi.h) - Win32 apps | Microsoft Docs
参考「Changing Privileges in a Token - Win32 apps | Microsoft Docs

3.5. 自プロセスしか扱えないファイル名取得関数に注意

GetWindowModuleFileNameW() 関数や GetModuleFileNameW() 関数 (Ex なし) は、自プロセス以外では実行ファイル名を取得できません。

自プロセスで生成したウィンドウのハンドルから実行ファイル名を取得するのはあまりしない思うので、多くの場合は GetModuleFileNameExW() 関数や QueryFullProcessImageNameW() 関数等を使用することになると思います。

参考「GetWindowModuleFileNameW function (winuser.h) - Win32 apps | Microsoft Docs
参考「GetModuleFileNameW function (libloaderapi.h) - Win32 apps | Microsoft Docs

3.6. PSAPI のバージョンによるコンパイルオプションやライブラリのリンクの違いについて

Windows 7 以降で PSAPI のバージョンが増え、バージョン 2 でライブラリ psapi の機能が kernel32 に吸収されたため、古い Windows で動作させる必要がない場合は PSAPI_VERSION=2 のようにします。

kernel32 のライブラリは Windows 向けのコンパイラ (MinGW-w64 等も含む) ではデフォルトでリンクされるため、オプションでのライブラリのリンク指定は不要です。

古い Windows で動かす必要がある場合は PSAPI_VERSION=1 とし、psapi.lib をリンクします。

参考「GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs

※本記事のコードでは Windows Vista 以降で PSAPI を使用しないため、実質 PSAPI_VERSION=2を使用しないようになっています。

3.7. プロセスのアクセス権限について

Windows Vista 以降で PROCESS_QUERY_INFORMATION 権限より制限が大きい PROCESS_QUERY_LIMITED_INFORMATION が利用できます。
今回のプログラムではどちらを利用しても動作は変わりませんが、権限は本来最低限のもののみ許可されるべきなので、利用できる環境なら PROCESS_QUERY_LIMITED_INFORMATION を利用した方が良いと思います。

Microsoft Docs によると、GetModuleFileNameExW() 関数については追加で PROCESS_VM_READ 権限も必要と書かれています。
Windows 10 で動作確認すると、モジュールハンドルが NULL の場合に (?) PROCESS_VM_READ 権限なしでも実行ファイル名を取得できますが、古い Windows でも同様に処理されるか分からないため、念のため付けておいた方が安全かと思います。

参考「Process Security and Access Rights - Win32 apps | Microsoft Docs
参考「GetModuleFileNameExW function (psapi.h) - Win32 apps | Microsoft Docs
参考「QueryFullProcessImageNameW function (winbase.h) - Win32 apps | Microsoft Docs

3.8. プロセスクローズを忘れないように注意

プログラミング全般で言えることですが、オープン後に別のエラーが発生した場合、クローズを忘れないように注意が必要です。

5
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?