列表1-4列出了EnumDeviceDrivers()一种可能的实现方式。注意这并不是来自psapi.dll的原始代码。但通过C编译器它可以变成等效的二进制代码。为了保持简单干净,我省略了源代码中易分散注意力的细节,比如结构化异常等。在列表1-4的中间,你会看到NtQuerySystemInformation()函数作了很多工作。这是我非常喜欢的windows 2000函数之一,因为该函数可以访问多种重要的数据结构,如驱动、进程、线程、句柄(handle)和LPC端口列表等等。我的文章“Inside Windows NT Sytem Data”(出版于1999年11月的Dr.Dobb’s Journal)在第一时间提供了有关该函数的内部信息及其搭档函数NtSetSystemInformation()的文档化资料。另外的全面讲述这两个函数的文档可以在Gary Nebbett的《Indispendsable Windows NT/2000 Native API Reference》中找到。
不要过于担心列表1-4列出的EnumDeviceDrivers()函数的实现细节。我增加这些代码片断只是为了例举该函数有趣的一面,这像一根红线贯穿于psapi.dll。在使用SystemModuleInformation标志第二次调用NtQuerySystemInformation()获取了完整的驱动列表后,代码遍历驱动模块数组并将其pImageBase成员复制到调用者提供的指针数组(名为lpImageBase[])中。这似乎很正确,但除非你不知道NtQuerySystemInformation提供的模块数组所包含的其他信息。这些数据结构都是没有文档化的,但是我现在可以告诉你,这些信息同样是有关模块在内存中的大小、它们的路径和名称、引用计数(load counts)和其他一些标志信息的。甚至文件名在路径中的偏移量也是很容易就能得到的!,EnumDeviceDrivers()残忍的丢掉了所有这些有用的信息,仅仅保留了映像基址(Image Base address)。
所以如果你试图通过返回的指针来获取有关模块的更多信息,则肯定会失败。当你调用GetDeviceDriverFileName()来获取指定映像基址对应的文件路径时,猜猜psapi.dll会怎样做?它会运行与列表1-4类似的代码来获取完整的驱动列表,并遍历该列表来寻找指定的映像基址。如果它找到一个匹配项,就将其路径复制到调用者的缓冲区中。这难道很高效吗?为什么EnumDeviceDrivers不在它首次遍历驱动列表时就复制路径呢?按这样的方式实现此函数并没有多么困难。除去性能问题,这种设计还有另一个潜在的问题:如果在GetDeviceDriverFileName()执行之前指定的模块就已经被卸载了会怎么样呢?该模块的地址将不会出现在第二次获取的驱动列表中,GetDeviceDriverFileName()将会失败。我真不明白微软为什么会发布这样的DLL。
枚举活动进程
psapi.dll的另一个典型工作就是枚举当前系统中运行的进程。为此目的,该DLL提供了EnumProcesses()函数。该函数的工作与EnumDeviceDrivers()十分类似,不过返回的是进程ID而不是虚拟地址了。再次提示,该函数并不会提示缓冲区大小不足,因此我们还需再次使用trial-and-error循环,如列表1-5所示,这些代码和列表1-3很相似,除了有些不同的符号和类型名称。
一个进程ID是一个全局数字标签可在整个系统中唯一标识一个进程。进程和线程ID都取自同一个数字池(pool of numbers),从以0开始的Idle进程,在同一时间,所有运行的进程和线程都不会有相同的ID。但是,当一个进程结束后,另一个进程可能会再次使用该结束进程或线程的ID。因此,在X时间获取的一个进程ID在Y时间可能会代表另一个完全不同的进程。也有可能在其使用的那一刻还没有定义或者指定给了某个线程。所以,EnumProcesses()返回一个简单的进程ID列表并不能可靠的代表当前系统活动进程的快照。如果考虑该函数的实现方式,这个设计缺陷真是无法原谅。列表1-6是psapi.dll另一个函数的克隆,大致勾勒出了EnumProcessees()的基本动作。和EnumDeviceDrivers()类似,它也依赖NtQuerySystemInformation()函数,不过在调用时,用SystemProcessInformation代替了SystemModuleInformation。注意列表1-6中间的循环,在哪儿lpidProcess[]数组被来自SYSTEM_PROCESS_INFORMATION结构中的数据填充。没什么好惊奇的,该结构也没有文档化。
BOOL WINAPI EnumProcesses( DWORD* lpidProcess,
DWORD cb,
DWORD* lpcbNeeded);
PDWORD WINAPI dbgProcessIds( PDWORD pdCount )
{
DWORD dSize;
DWORD dCount = 0;
PDWORD pdList = NULL;
dSize = SIZE_MINIMUM * sizeof( DWORD );
while ( (pdList = dbgMemoryCreate(dSize)) != NULL )
{
if ( EnumProcesses( pdList, dSize, &dCount) && (dCount < dSize) )
{
dCount /= sizeof( DWORD );
break;
}
dCount = 0;
pdList = dbgMemoryDestroy(pdList);
if ( (dSize <<= 1) > (SIZE_MXAIMUM*sizeof(DWORD)) ) break;
}
if ( pdCount != NULL ) *pdCount = dCount;
return pdList;
}
列表1-5 枚举进程ID
在看过EnumDeviceDrivers()是如何浪费从NtQuerySystemInformation()返回的数据后,不幸的是,EnumProcesses也是和其类似的函数,但,事实上,这个函数更糟糕!因为可用的进程信息要远多于驱动模块的信息,因为进程数据之后还包含很多有关系统中每个线程的详细信息。在我写下这段文字时,我的系统正运行着37个进程,调用NtQuerySystemInformation()产了一个24,488字节的数据块!而当EnumProcesses()处理完这些数据后,仅剩下了148字节,这些刚好够存放37个进程ID。
尽管EnumDeviceDirvers()让我有些难过,但EnumProcesses()却真正伤害了我的心。如果你需要使用未文档化API函数的理由,那这两个函数就是最好的证据。如果实际的工作只需一步既可完成,那为什么还要使用如此低效的函数呢?为什么不自己调用NtQuerySystemInformation()函数自由的获取感兴趣的系统信息?微软提供的许多系统管理工具都依赖于NtQuerySystemInformation()而不是psapi.dll,so why settle for less?
BOOL WINAPI EnumProcesses( PDWORD lpidProcess,
DWORD cb,
PDWORD lpcbNeeded)
{
PSYSTEM_PROCESS_INFORMATION pspi, pSpiNext;
DWORD dSize, i;
NTSTATUS ns;
BOOL fOk = FALSE;
// 0x8000 = 32KB
for (dSize=0x8000; ((pspi = LocalAlloc(LMEM_FIXED,dSize)) != NULL);
dSize += 0x8000)
{
ns = NtQuerySystemInformation( SystemProcessInformation,pspi,
dSize, NULL);
if ( STATUS_SUCCESS == ns )
{
pSpiNext = pspi;
for ( i=0; i < cb/sizeof(DWORD); i++ )
{
lpidProcess[i] = pspiNext->dUniqueProcessId;
pSpiNext = (PSYSTEM_PROCESS_INFORMATION)
((BYTE)pSpiNext+pSpiNext->dNext);
}
*lpcbNeeded = i * sizeof(DWORD);
fOk = TRUE;
}
LocalFree(pspi);
if ( fOk || (ns != STATUS_INFO_LENGTH_MISMATCH) )
{
if ( !fOk) SetLastError(RtlNtStatusToDosError(ns));
break;
}
return fOk;
}
列表1-6 EnumProcesses()函数的示例实现
枚举进程模块
一但你从EnumProcess()返回的进程列表中发现了你感兴趣的进程ID,你可能会想知道在此进程的虚拟地址空间中加载了哪些模块。psapi.dll提供了另一个API函数来完成此功能,叫做EnumProcessModules()。与EnumDeviceDrivers()和EnumProcesses()不同,这个函数需要四个参数(参见列表1-7)。不同于前两个返回系统全局列表的函数,EnumProcessModules()只取回指定进程的列表,因此,增加的那个参数唯一表示一个进程。然而,该函数需要一个进程句柄(HANDLE)来代替进程ID。为了通过进程ID获取其句柄(HANDLE),必须调用OpenProcess()函数。
BOOL WINAPI EnumProcessModule( HNADLE hProcess,
HMODULE* lphModule,
DWORD cb,
DWORD* lpcbNeeded);
PHMODULE WINAPI dbgProcessModules( HANDLE hProcess, PDWORD pdCount)
{
DWORD dSize;
DWORD dCount = 0;
PHMODULE phList = NULL;
if ( hProcess != NULL )
{
dSize = SIZE_MINIMUM * sizeof( HMODULE );
while ( (phList = dbgMemoryCreate(dSize)) != NULL )
{
if ( EnumProcessModules(hProcess,phList,dSize,&dCount))
{
if (dCount <= dSize)
{
dCount /= sizeof( HMODULE );
break;
}
}
else
{
dCount = 0;
}
phList = dbgMemoryDestroy(phList);
if ( !(dSize = dCount) ) break;
}
}
if ( pdCount != NULL) *pdCount = dCount;
return phList;
}
列表1-7 枚举进程模块