在Windows操作系统中,枚举当前运行的进程是一个常见的需求,但遗憾的是,并没有一个统一的标准方法来实现这一功能。在Windows 95和Windows 98中,可以使用ToolHelp API函数(位于Kernel32.dll中)。然而,微软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
}
类中有七个公共函数:
枚举方法相关的函数设置/返回在ENUM_METHOD命名空间中找到的值之一:
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添加到枚举方法中。这通常默认完成。
这些函数接受一个指向CProcessEntry/CModuleEntry的指针作为输入,并根据枚举成功或失败返回TRUE/FALSE。类中有用的成员如下:
CProcessEntry {
LPTSTR lpFilename; // 文件名
DWORD dwPID; // 进程ID
WORD hTask16; // 如果这是一个16位进程,返回任务句柄,否则为0
}
成员hTask16仅在NT和Win2k上使用。如果在这些操作系统上hTask16不为0,则这是一个16位进程。在这种情况下,lpFilename将是16位进程的路径,而dwPID将是当前运行的NTVDM的标识符(参见"关于16位进程的说明")。
发现许多模块没有加载到它们的首选基址。如果一个模块没有加载到它的首选基址,使用它的应用程序将需要更多的内存,并在初始化时遭受性能损失。这是因为只有当基址等于加载地址时,模块才能映射到磁盘上的.dll上。
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};
ReadProcessMemory(hProc, pModBase, &idh, sizeof(idh), NULL);
if (IMAGE_DOS_SIGNATURE == idh.e_magic) {
ReadProcessMemory(hProc, (PBYTE)pModBase + idh.e_lfanew, &inh, sizeof(inh), NULL);
CloseHandle(hProc);
if (IMAGE_NT_SIGNATURE == inh.Signature)
return (PVOID) inh.OptionalHeader.ImageBase;
}
}
return NULL;
}
对于不熟悉PE格式的人来说,这些是可执行文件的标准头。当可执行文件加载到内存中时,整个文件都会被映射,包括头。这里可以找到很多有用的信息,比如映射文件的大小。
在Windows 95和Windows 98中,ToolHelp API像处理任何其他进程一样处理16位应用程序。在NT下则不是这样。相反,CEnumProcess将返回它当前运行的NT虚拟DOS机(NTVDM)的名称。这意味着如果运行测试应用程序并找到一些像"ntvdm.exe"这样的进程名,就找到了一个16位进程。如果ENUM_METHOD::PROC16当前被设置,GetNextProcess返回的下一个条目将是这个虚拟机中当前运行的16位进程。
有些进程具有安全属性,可以防止读取其内存。这意味着无法枚举模块。如果使用PSAPI,甚至无法检索到文件名。