轉自:http://www.blog.edu.cn/user2/33587/archives/2005/254906.shtml
代碼下載:說明 ProcessSpy.zip
當程序出現異常而失去響應,我們通常的做法是打開Windows任務管理器強行將其“殺死”。Windows任務管理器是個好東西,它能顯示當前系統中運行的所有進程,以及它們的實時性能參數。但是作為程序員,你知道這些功能是怎么實現的嗎?
“這
有什么難的?!”你可能會說,“不就是調用那幾個進程枚舉函數嘛!”是啊,單純實現Windows任務管理器類似的功能是不難。但是,你先別急,關于進程
枚舉,可能你只知其一,不知其二;更何況,我們這里還有其三、其四。除此之外,我們這里還要增強功能,顯示與各個進程相關聯的模塊(即DLL,動態鏈接
庫)信息。
進程與DLL的基礎知識
大家知道,Windows 98/2000/XP都是多任務操作系統。所謂多任務,就是系統中可以同時運行多個進程。而所謂進程,就是應用程序的運行實例。通俗地講,進程就是一個運行起來的.EXE程序。
系統中的進程都用一個DWORD類型的數據來唯一標識,我們稱之為PID。即使同一個應用程序運行多個實例,它們的PID也是不一樣的。另外,進程擁有自己私有的虛擬地址空間,進程與進程之間不會相互干擾;每個進程都至少包含一條線程。
那么,DLL與進程又有什么關系呢?大家知道,自Windows誕生之日起,Windows操作系統就使用DLL來支持公共函數調用。DLL中實現的函數代碼不出現在.EXE文件中,但可以被各個進程所使用。
使用DLL的好處包括:
1) 可以顯著地減小每個組件的大小(特別是對于一些大型軟件系統)。
2) 使升級更為簡單。如果我們想要使用新版本的函數,改變DLL中的函數后,只需重新編譯DLL項目,然后再連接使用該函數的各個應用程序;而應用程序本身不需要重新編譯。
3) 便于功能模塊化,乃至開發任務的團隊協作。
一般來說,一個進程總是調用這個或那個DLL中的函數。進程與DLL是一種依賴關系。在我們的演示程序中,我們不僅要做進程枚舉,我們還要來揭示進程與DLL的這種依賴關系。演示程序的用戶界面如下:

圖1 演示程序之用戶界面
好了,言歸正轉,我們直奔主題。接下去,我們就來逐一介紹各種進程枚舉方法。
方法一:使用工具庫(Tool Help Library)函數
這是一種歷史最悠久、也是最基本的方法(從Windows 95開始就支持這種方法)。這些API函數中,最重要的當屬CreateToolhelp32Snapshot,它的函數原型如下:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
這
個函數的功能就是給系統拍“快照”。拍照的對象由參數dwFlags決定,比如dwFlags值為TH32CS_SNAPPROCESS表示對象為系統中
的所有進程,值為TH32CS_SNAPMODULE表示對象為由th32ProcessID參數指定的進程調用的所有模塊(也就是DLL)。
當
調用CreateToolhelp32Snapshot函數給指定的對象拍完快照之后,我們就可以使用Process32First、
Process32Next、Module32First、Module32Next等函數進行“取片”工作了,就是遍歷剛才拍下來的所有進程、進程調用
的所有模塊。
我們的演示程序提供了完整的代碼實現:
BOOL CToolHelpSpy::BuildProcessList(void)
{
// 給系統中所有進程拍快照
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
PROCESSENTRY32 pe32 = {0};
pe32.dwSize = sizeof(PROCESSENTRY32);
// 遍歷拍下來的所有進程
if (Process32First(hProcessSnap, &pe32))
{
do
{
if (pe32.th32ProcessID && strcmp(pe32.szExeFile, "System"))
{
// 保存進程的名字、PID
CProcessItem processItem;
processItem.SetProcessName(pe32.szExeFile);
processItem.SetProcessId(pe32.th32ProcessID);
// 加入列表保存
mProcList.AddTail(processItem);
}
} while (Process32Next(hProcessSnap, &pe32));
}
CloseHandle(hProcessSnap);
return TRUE;
}
BOOL CToolHelpSpy::BuildModuleList(CProcessItem& inProcess)
{
// 給指定的進程調用的所有模塊拍快照
HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,
inProcess.GetProcessId());
if (hModuleSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
MODULEENTRY32 me32 = {0};
me32.dwSize = sizeof(MODULEENTRY32);
inProcess.CleanupModuleList();
// 遍歷所有模塊
if (Module32First(hModuleSnap, &me32))
{
do
{
// 保存模塊文件全路徑
inProcess.AddModuleItem(me32.szExePath);
} while (Module32Next(hModuleSnap, &me32));
}
CloseHandle(hModuleSnap);
return TRUE;
}
注:工具庫函數在Kernel32.dll中實現。程序開發中,我們需要包含頭文件Tlhelp32.h,連接庫文件Kernel32.lib。
注:
我們這里使用自定義類CProcessItem來描述一個進程,它保存了進程的名字、PID等信息,另外還維持一個該進程調用的所有模塊的列表。相應地,
我們也使用一個自定義類CModuleItem來描述模塊,它保存模塊文件的全路徑、版本號、文件大小、說明信息、所屬產品名等。(下同)
方法二:使用PSAPI (Process Status API)函數
這是一種Windows NT/2000下的方法。核心是使用EnumProcesses函數。它的原型如下:
BOOL EnumProcesses(
DWORD *lpidProcess, // 用于保存所有進程的PID的數組
DWORD cb, // 上述數組的大小
DWORD *cbNeeded // PID數組中實際返回的(有效)字節數
);
當
獲得系統中所有進程的PID后,我們就可以使用OpenProcess函數打開指定的進程,再調用GetModuleBaseName獲得該進程的名字,
調用EnumProcessModules枚舉該進程調用的所有模塊,調用GetModuleFileNameEx獲得模塊文件的全路徑。
我們的演示程序提供了完整的代碼實現:
BOOL CPSApiSpy::BuildProcessList(void)
{
// 枚舉獲得系統中的所有進程的PID
DWORD processes[1024], needed;
if (!EnumProcesses(processes, sizeof(processes), &needed))
{
return FALSE;
}
char szName[MAX_PATH] = "";
DWORD actualProcessCount = needed / sizeof(DWORD);
for (DWORD i = 0; i < actualProcessCount; i++)
{
// 保存進程的PID
CProcessItem processItem;
processItem.SetProcessId(processes[i]);
// 打開當前進程以獲得進程操作句柄
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, processes[i]);
if (hProcess)
{
HMODULE hModule;
DWORD needed;
// 枚舉當前進程調用的所有模塊
if (EnumProcessModules(hProcess, &hModule, sizeof(hModule), &needed))
{
// 獲得并保存進程的名字
GetModuleBaseName(hProcess, hModule, szName, sizeof(szName));
processItem.SetProcessName(szName);
mProcList.AddTail(processItem);
}
CloseHandle(hProcess);
}
}
return TRUE;
}
BOOL CPSApiSpy::BuildModuleList(CProcessItem& inProcess)
{
// 根據PID打開該進程,獲得一個進程操作句柄
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, inProcess.GetProcessId());
if (hProcess)
{
HMODULE modules[1024];
DWORD needed;
// 枚舉當前進程調用的所有模塊
if (EnumProcessModules(hProcess, modules, sizeof(modules), &needed))
{
char szName[MAX_PATH] = "";
inProcess.CleanupModuleList();
DWORD actualModuleCount = needed / sizeof(DWORD);
// 獲得各個模塊文件的全路徑
for (DWORD i = 1; i < actualModuleCount; i++)
{
GetModuleFileNameEx(hProcess, modules[i], szName, sizeof(szName));
inProcess.AddModuleItem(szName);
}
}
CloseHandle(hProcess);
}
return TRUE;
}
注:PSAPI函數在Psapi.dll中實現。程序開發中,我們需要包含頭文件Psapi.h,連接庫文件Psapi.lib。這些文件在安裝了微軟的Platform SDK后就可獲得。
方法三:利用系統收集的性能數據(Performance Data)
這也是一種Windows NT/2000下的方法。首先,我們需要介紹一些關于性能監視(Performance Monitoring)的背景知識。
所
謂性能監視,實際上是Windows
NT/2000提供的一種系統功能,它能實時采集、分析系統內的應用程序、服務、驅動程序等的性能數據,以此來分析系統的瓶頸、監視組件的表現,最終幫助
用戶進行系統的合理調配。這里,還要引入一個性能對象(Performance
Object)的概念,即被監視者。一般系統中的性能對象包括處理器(Processor)、進程(Process)、線程(Thread)、網絡通訊
(如TCP、UDP、ICMP、IP等)、系統服務(如ACS/RSVP
Service)等。(本文我們關心的是進程,即名為“Process”的對象。)下面,我們給出系統性能數據的結構參考圖:

圖2 系統性能數據的結構
性
能對象有兩種:一種只支持一個實例,另一種支持多個實例。(我們關心的進程對象支持多個實例,而每個實例對應系統中的一個進程。)一個對象可以有多個性能
指標;每個性能指標都用一個計數器(Counter)來記錄。就進程對象而言,它擁有的計數器種類包括ID
Process(進程的PID)、Thread Count(線程數)、Priority Base(進程優先級)、IO Read
Bytes/sec(每秒IO讀取字節數)、IO Writer Bytes/sec(每秒IO寫出字節數)等。(本文我們只關心ID
Process計數器的值。)
支持單一實例的對象數據結構如下(也就是圖2中各個對象數據塊的展開形式):

圖3 支持單一實例的對象數據結構
支持多實例的對象數據結構如下(增加了各個實例的定義部分):

圖4 支持多實例的對象數據結構
知
道了性能數據結構,接下去我們怎么來讀取它們呢?最基本的方法就是通過注冊表函數,如RegOpenKeyEx、RegQueryValueEx、
RegCloseKey等。值得注意的是,這里雖然使用的是注冊表函數,但性能數據并不存儲在注冊表數據庫中;讀取性能數據時調用函數
RegOpenKeyEx,主鍵值應該是HKEY_PERFORMANCE_DATA。而當性能數據獲得之后,根據各部分數據結構的定義,計算偏移量,我
們就能獲取我們感興趣的數據了。
我們的演示程序提供了完整的代碼實現:
#define INITIAL_SIZE 51200
#define EXTEND_SIZE 25600
#define REGKEY_PERF _T("Software\\Microsoft\\Windows NT\\Currentversion\\Perflib")
#define REGSUBKEY_COUNTERS _T("Counters")
#define PROCESS_COUNTER _T("process")
#define PROCESSID_COUNTER _T("id process")
BOOL CPerformanceSpy::BuildProcessList(void)
{
// 步驟一:從特定的注冊表路徑下獲取系統中所有的對象、計數器的名字
LANGID lid = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL);
TCHAR szSubKey[1024];
_stprintf(szSubKey, _T("%s\\%03x"), REGKEY_PERF, lid);
HKEY hSubKey;
DWORD rt = RegOpenKeyEx(HKEY_LOCAL_MACHINE, szSubKey, 0,
KEY_READ, &hSubKey);
if (rt != ERROR_SUCCESS)
{
return FALSE;
}
DWORD dwType = 0;
DWORD dwSize = 0;
LPBYTE buffer = NULL;
BOOL pass = FALSE;
// 獲得裝載所有計數器名字的緩沖大小
rt = RegQueryValueEx(hSubKey, REGSUBKEY_COUNTERS, NULL,
&dwType, NULL, &dwSize);
if (rt == ERROR_SUCCESS)
{
buffer = (LPBYTE) malloc(dwSize);
memset(buffer, 0, dwSize);
rt = RegQueryValueEx(hSubKey, REGSUBKEY_COUNTERS, NULL,
&dwType, buffer, &dwSize);
}
LPSTR p, p2;
DWORD dwProcessIdTitle;
DWORD dwProcessIdCounter;
PPERF_DATA_BLOCK pPerf;
PPERF_OBJECT_TYPE pObj;
PPERF_INSTANCE_DEFINITION pInst;
PPERF_COUNTER_BLOCK pCounter;
PPERF_COUNTER_DEFINITION pCounterDef;
if (rt == ERROR_SUCCESS)
{
pass = TRUE;
// 步驟二:查找名為“Process”的對象以及名為“ID Process”的計數器
// 獲取它們的索引值(因為對象、計數器在性能數據中是靠索引來標識的)
p = (LPSTR) buffer;
while (*p)
{
if (p > (LPSTR) buffer)
{
for (p2 = p - 2; _istdigit(*p2); p2--)
;
}
if (_tcsicmp(p, PROCESS_COUNTER) == 0)
{
// 獲取“Process”對象的索引
for (p2 = p - 2; _istdigit(*p2); p2--)
;
_tcscpy(szSubKey, p2+1);
}
else if (stricmp(p, PROCESSID_COUNTER) == 0)
{
// 獲取“ID Process”計數器的索引
for (p2 = p - 2; _istdigit(*p2); p2--)
;
dwProcessIdTitle = atol(p2 + 1);
}
// Point to the next string
p += (_tcslen(p) + 1);
}
// 步驟三:獲取進程對象的所有性能數據
free(buffer);
buffer = NULL;
dwSize = INITIAL_SIZE;
buffer = (LPBYTE) malloc(dwSize);
memset(buffer, 0, dwSize);
while (pass)
{
rt = RegQueryValueEx(HKEY_PERFORMANCE_DATA, szSubKey, NULL,
&dwType, buffer, &dwSize);
pPerf = (PPERF_DATA_BLOCK) buffer;
// 性能數據塊開頭以四個字符“PERF”標識
if ((rt == ERROR_SUCCESS) && (dwSize > 0) &&
pPerf->Signature[0] == (WCHAR)'P' &&
pPerf->Signature[1] == (WCHAR)'E' &&
pPerf->Signature[2] == (WCHAR)'R' &&
pPerf->Signature[3] == (WCHAR)'F')
{
break;
}
// 如果緩沖不夠大,擴大緩沖后再試
if (rt == ERROR_MORE_DATA)
{
dwSize += EXTEND_SIZE;
buffer = (LPBYTE) realloc(buffer, dwSize );
memset(buffer, 0, dwSize );
}
else
{
pass = FALSE;
}
}
}
if (pass)
{
pObj = (PPERF_OBJECT_TYPE) ((DWORD)pPerf + pPerf->HeaderLength);
// 步驟四:在進程對象數據的計數器定義部分尋找“ID Process”計數器
pCounterDef = (PPERF_COUNTER_DEFINITION) ((DWORD)pObj + pObj->HeaderLength);
for (DWORD i = 0; i < (DWORD)pObj->NumCounters; i++)
{
if (pCounterDef->CounterNameTitleIndex == dwProcessIdTitle)
{
dwProcessIdCounter = pCounterDef->CounterOffset;
break;
}
pCounterDef++;
}
// 步驟五:遍歷所有實例,獲取實例的名字(即進程名)以及PID
TCHAR szProcessName[MAX_PATH];
pInst = (PPERF_INSTANCE_DEFINITION) ((DWORD)pObj + pObj->DefinitionLength);
for (i = 0; i < (DWORD)pObj->NumInstances; i++)
{
// 獲取進程名
p = (LPSTR) ((DWORD)pInst + pInst->NameOffset);
rt = WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)p, -1, szProcessName,
sizeof(szProcessName), NULL, NULL);
// 獲取進程PID
pCounter = (PPERF_COUNTER_BLOCK) ((DWORD)pInst + pInst->ByteLength);
DWORD processId = *((LPDWORD) ((DWORD)pCounter + dwProcessIdCounter));
if (strcmp(szProcessName, "System") && processId)
{
CProcessItem processItem;
processItem.SetProcessId(processId);
processItem.SetProcessName(szProcessName);
mProcList.AddTail(processItem);
}
// Point to the next process
pInst = (PPERF_INSTANCE_DEFINITION) ((DWORD)pCounter + pCounter->ByteLength);
}
}
if (buffer)
{
free(buffer);
buffer = NULL;
}
RegCloseKey(hSubKey);
RegCloseKey(HKEY_PERFORMANCE_DATA);
return pass;
}
注:方法三用到的僅僅是注冊表操作函數,而這些函數在advapi32.dll中實現。程序開發中,我們需要包含頭文件winperf.h。另外,該方法中各個進程所調用的模塊,仍然使用方法二的PSAPI函數獲得,這里就不再列出。
方法四:使用PDH (Performance Data Helper)函數
這種方法的底層實現跟方法三其實是一樣的。但我們看到,方法三實現起來非常繁瑣。為了簡化應用,PDH函數對方法三的實現進行了一層封裝。我們這里的進程枚舉,主要使用PdhEnumObjectItems函數,它的函數原型如下:
PDH_STATUS PdhEnumObjectItems(
LPCTSTR szDataSource, // 數據源
LPCTSTR szMachineName, // 機器名
LPCTSTR szObjectName, // 對象名
LPTSTR mszCounterList, // 計數器列表
LPDWORD pcchCounterListLength, // 計數器列表長度
LPTSTR mszInstanceList, // 實例列表
LPDWORD pcchInstanceListLength, // 實例列表長度
DWORD dwDetailLevel, // 獲取信息的級別
DWORD dwFlags // 保留為0
);
對
于每一個獲得的進程實例,我們還要得到它的PID,也就是得到“ID
Process”計數器的值。這時,我們會用到其他的PDH函數,包括:PdhOpenQuery、PdhAddCounter、
PdhCollectQueryData、PdhGetFormattedCounterValue、PdhCloseQuery等。
我們的演示程序提供了完整的代碼實現:
BOOL CPDHSpy::BuildProcessList(void)
{
LPTSTR szCounterListBuffer = NULL;
DWORD dwCounterListSize = 0;
LPTSTR szInstanceListBuffer = NULL;
DWORD dwInstanceListSize = 0;
BOOL pass = FALSE;
// 第一次調用PdhEnumObjectItems以獲取需要的列表長度
PDH_STATUS pdhStatus = PdhEnumObjectItems(NULL, NULL, TEXT("Process"),
szCounterListBuffer, &dwCounterListSize, szInstanceListBuffer,
&dwInstanceListSize, PERF_DETAIL_WIZARD, 0);
if (pdhStatus == ERROR_SUCCESS)
{
szCounterListBuffer = (LPTSTR) malloc((dwCounterListSize * sizeof (TCHAR)));
szInstanceListBuffer = (LPTSTR) malloc((dwInstanceListSize * sizeof (TCHAR)));
// 第二次調用PdhEnumObjectItems
// 獲得“Process”對象的所有計數器和實例
pdhStatus = PdhEnumObjectItems(NULL, NULL, TEXT("Process"),
szCounterListBuffer, &dwCounterListSize, szInstanceListBuffer,
&dwInstanceListSize, PERF_DETAIL_WIZARD, 0);
if (pdhStatus == ERROR_SUCCESS)
{
pass = TRUE;
LPTSTR pInst = szInstanceListBuffer;
// 獲得每個實例名,也就是進程名
for (; *pInst != 0; pInst += lstrlen(pInst) + 1)
{
if (strcmp(pInst, "System") && strcmp(pInst, "Idle") &&
strcmp(pInst, "_Total"))
{
CProcessItem processItem;
// 獲得進程的PID
processItem.SetProcessId(GetPIDCounterValue(pInst));
processItem.SetProcessName(pInst);
mProcList.AddTail(processItem);
}
}
}
}
if (szCounterListBuffer != NULL)
{
free(szCounterListBuffer);
szCounterListBuffer = NULL;
}
if (szInstanceListBuffer != NULL)
{
free(szInstanceListBuffer);
szInstanceListBuffer = NULL;
}
return pass;
}
DWORD CPDHSpy::GetPIDCounterValue(LPTSTR inInstanceName)
{
// 打開一個查詢對象
HQUERY hQuery = NULL;
PDH_STATUS pdhStatus = PdhOpenQuery (0, 0, &hQuery);
HCOUNTER hCounter = NULL;
char szPathBuffer[MAX_PATH];
sprintf(szPathBuffer, "\\Process(%s)\\ID Process", inInstanceName);
pdhStatus = PdhAddCounter(hQuery, szPathBuffer, 0, &hCounter);
pdhStatus = PdhCollectQueryData(hQuery);
// 獲得當前實例的“ID Process”計數器的值
DWORD ctrType;
PDH_FMT_COUNTERVALUE fmtValue;
pdhStatus = PdhGetFormattedCounterValue(hCounter, PDH_FMT_LONG,
&ctrType, &fmtValue);
// 關閉查詢對象
pdhStatus = PdhCloseQuery (hQuery);
return fmtValue.longValue;
}
注:PDH函數在Pdh.dll中實現。程序開發中,我們需要包含頭文件Pdh.h,連接庫文件Pdh.lib。
演示程序說明
我們的演示程序使用VC6.0開發完成,是一個基于對話框的MFC程序。程序設計秉承OOP風格,以及用戶界面(User Interface)與業務邏輯(Business Logic)分離的原則,結構簡單、條理清晰,相信大家很容易能夠讀懂代碼。
由于本文總共介紹了四種進程枚舉的方法,我們設計了如下一個邏輯控制類繼承結構:

圖5 演示程序邏輯控制類結構
另外,演示程序對于進程調用的模塊采用了延后枚舉(Lazy Enumerating)的策略,即在程序啟動的時候并沒有將所有進程調用的模塊都枚舉好,而僅在需要的時候進行。這樣可以顯著節省程序啟動的時間。
寫在最后
進程隱藏(與其相對的就是進程枚舉)一直是一個很熱門的話題,思路有很多,其中有一種就是攔截系統API函數EnumProcesses的調用。通讀本文后,你覺得這種思路可行嗎?或者你有了其他新的想法!這些都是筆者寫作此文的初衷。
正文完
附件: