NT/Win9x 上枚举进程






4.94/5 (13投票s)
2001 年 11 月 23 日
4分钟阅读

233287

3505
一个封装 tlhelp32 和 PSAPI 的简单类
动机
许多程序员编写需要枚举正在运行进程的软件。不幸的是,这没有标准的方法。在 Windows 95 和 Windows 98 上,有 ToolHelp API 函数(位于 Kernel32.dll 中)。出于某种原因,Microsoft NT 团队不喜欢 ToolHelp 函数,并决定不在 Windows NT 中添加它们。取而代之的是,他们提供了自己的进程状态函数集 PSAPI,并将其添加到外部模块(位于 psapi.dll 中)。最后,在 Windows 2000 上,开发人员选择了同时提供这两种枚举方法。
这是什么
CEnumProcess
是一个简单的类,用于使用 PSAPI 或 ToolHelp 枚举正在运行的进程。首选方法在运行时确定。它包含两个类:CEnumProcess::CProcessEntry
和 CEnumProcess::CModuleEntry
用于存储结果。工作原理
在创建类的实例时,它会尝试加载适当的模块并查找与 PSAPI/ToolHelp 相关的函数。根据找到的函数集,类会将枚举方法设置为最合适的。例如,这是查找与 PSAPI 相关的函数的代码:
// Try to load psapi.dll PSAPI = ::LoadLibrary(TEXT("PSAPI")); if (PSAPI) { // Find PSAPI functions FEnumProcesses = (PFEnumProcesses)::GetProcAddress(PSAPI, TEXT("EnumProcesses")); FEnumProcessModules = (PFEnumProcessModules)::GetProcAddress(PSAPI, TEXT("EnumProcessModules")); #ifdef UNICODE FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, TEXT("GetModuleFileNameExW")); #else FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, TEXT("GetModuleFileNameExA")); #endif }
用法
该类中有七个公共函数int GetAvailableMethods()
int GetSuggestedMethod()
int SetMethod(int method)
BOOL GetProcessNext(CProcessEntry *pEntry)
BOOL GetProcessFirst(CProcessEntry* pEntry)
BOOL GetModuleNext(DWORD dwPID, CModuleEntry* pEntry)
BOOL GetModuleFirst(DWORD dwPID, CModuleEntry* pEntry)
namespace ENUM_METHOD {const int NONE = 0x0; const int PSAPI = 0x1; const int TOOLHELP= 0x2; const int PROC16 = 0x4; }
ENUM_METHOD::NONE
用于不太可能找不到任何方法的事件,例如 NT 用户删除了 psapi.dll。在 Windows 2000 上,GetAvailableMethods
返回 ENUM_METHOD::PSAPI + ENUM_METHOD::TOOLHELP
。建议的方法是使用 ToolHelp API。如果要枚举 16 位进程,应将 ENUM_METHOD::PROC16
添加到枚举方法中。这通常是默认执行的。
更令人感兴趣的是 GetProcess/GetModule 函数。它们接受 `CProcessEntry`/`CModuleEntry` 指针作为输入,并根据枚举的成功或失败返回 `TRUE`/`FALSE`。类中有用的成员如下:
CProcessEntry
LPTSTR lpFilename; // name of file DWORD dwPID; // ID of the process WORD hTask16; // If this is a 16-bit process, // return task handle here, otherwise 0
成员 `hTask16` 仅在 NT 和 Win2k 上使用。如果在这些操作系统上 `hTask16` 的值不是 0,则这是一个 16 位进程。在这种情况下,`lpFilename` 将是 16 位进程的路径,而 `dwPID` 将是当前运行的 NTVDM 的标识符(参见“关于 16 位进程的一点说明”)。
请注意,如果在 Win2k 上使用 ToolHelp,`lpFileName` 将不是文件的完整路径。尝试使用 `GetModuleFirst` 获取加载的第一个模块(即 .exe 本身),然后从 `CModuleEntry` 中检索完整路径。
CModuleEntry
LPTSTR lpFilename; // path of file PVOID pLoadBase; // Loading address PVOID pPreferredBase; // Address specified in fileheader
这一切意味着您可以编写如下代码,它可以在 Win9x 和 NT 上正常工作。
CEnumProcess enumeration; CEnumProcess::CProcessEntry entry; for (BOOL OK = enumeration.GetProcessFirst(&entry); OK; OK = enumeration.GetProcessNext(&entry) ) { // Do something useful TRACE("PID = %X, process = %s\n", entry.dwPID, entry.lpFilename); }
首选基址为何重要
我发现许多模块不会加载到其首选基址。如果模块未加载到其首选基址,则使用它的应用程序将需要更多内存,并在初始化时导致性能下降。这是因为只有当基址等于 .dll 在磁盘上的映射地址时,模块才能映射到 .dll。这是最初将首选基址包含在 moduleentry 中的原因。通常是由于懒惰的程序员,他们不从默认地址(0x10000000,或 Visual Basic 的 0x11000000)重新基址化他们的模块。模块在某个地方找到加载位置也很难预测。查找首选基址的函数如下所示:
PVOID CEnumProcess::GetModulePreferredBase(DWORD dwPID, PVOID pModBase) {if (ENUM_METHOD::NONE == m_method) return NULL; HANDLE hProc = OpenProcess(PROCESS_VM_READ, FALSE, dwPID); if (hProc) {IMAGE_DOS_HEADER idh = {0}; IMAGE_NT_HEADERS inh = {0}; //Read DOS header ReadProcessMemory(hProc, pModBase, &idh, sizeof(idh), NULL); if (IMAGE_DOS_SIGNATURE == idh.e_magic) // DOS header OK? // Read NT headers at offset e_lfanew ReadProcessMemory(hProc, (PBYTE)pModBase + idh.e_lfanew, &inh, sizeof(inh), NULL); CloseHandle(hProc); if (IMAGE_NT_SIGNATURE == inh.Signature) //NT signature OK? // Get the preferred base... return (PVOID) inh.OptionalHeader.ImageBase; } return NULL; //didn't find anything useful.. }对于 PE 格式的新手来说,这些是可执行文件的标准头。当可执行文件加载到内存中时,整个文件都会被映射,甚至包括头。这里可以找到很多有用的信息,例如映射文件的大小。
关于 16 位进程的一点说明
在 Windows 95 和 Windows 98 上,ToolHelp API 将 16 位应用程序视为与其他进程一样。在 NT 上情况并非如此。相反,`CEnumProcess` 将返回其当前运行的 NT 虚拟 DOS 机 (NTVDM) 的名称。这意味着,如果您运行测试应用程序并找到类似“ntvdm.exe”的进程名称,那么您就找到了一个 16 位进程。如果当前设置了 `ENUM_METHOD::PROC16`,那么 `GetNextProcess` 返回的下一条目将是当前在该虚拟机中运行的 16 位进程。这是通过使用 VDMDBG API 完成的,这是一组用于调试 16 位应用程序的函数。VDMDBG 允许枚举模块,但前提是进程充当调试器。这要求 16 位调试器 WOWDEB.EXE 任务在当前正在调试的 VM 中运行。但是,作为调试器附加并不是一个好主意。没有办法解除附加,并且如果调试器进程终止,被调试进程也会终止。因此,类中不包含 16 位模块的枚举。已知错误/限制
某些进程具有阻止读取其内存的安全属性。这意味着无法枚举模块。如果使用 PSAPI,甚至无法检索文件名。
联系方式
将建议、改进和评论发送给我:我。历史
2002 年 9 月 10 日 - 更新下载