四種方法實現VC枚舉系統當前進程

四種方法實現VC枚舉系統當前進程

中關村在線 【合作】 作者: 2005年04月27日 18:03

<script language="javascript" type="text/javascript">var fo = new SWFObject("http://pic.zol.com.cn/200704/mrs_systemcenter_clock_210x210_20k_0401.swf", "_AD5954", "210", "210", "7");fo.addParam("wmode", "opaque");fo.write("AD5954");</script>

適合讀者:程序員
前置知識:C語言基本閱讀能力

四種方法實現VC枚舉系統當前進程

    在Windows 2000以上的MS操作系統,通過Windows的任務管理器可以列出當前系統的所有活動進程(如圖1所示),在Windows XP中,更是在控制檯下增加了一條Tasklist命令,讓系統下的所有進程無所遁行(如圖2所示)。這一切是怎麼實現的呢?


四種方法實現VC枚舉系統當前進程
 
圖 1

 四種方法實現VC枚舉系統當前進程

圖 2


引用侯捷大師在《深入淺出MFC》的一句話,“知其然而不知其所以然,真不是個好辦法”。既然如此,我們乾脆自己動手,自己通過編程來實現吧,這樣很有成就感哦!以下所有代碼均在Windows XP SP1+VC6.0 sp6編譯環境下通過。Ok,Let’ go!

方法一

第一種方法是大家比較熟悉的通過ToolHelp Service提供的API函數來實現。這裏用到了3個關鍵的函數:CreateToolhelp32Snapshot(),Process32First()和Process32Next()。下面給出了關於這三個函數的原形和參數說明;

HANDLE WINAPI CreateToolhelp32Snapshot(
  DWORD dwFlags, //系統快照要查看的信息類型
  DWORD th32ProcessID      //值0表示當前進程
);
BOOL WINAPI Process32First(
  HANDLE hSnapshot,        //CreateToolhelp32Snapshot()創建的快照句柄
  LPPROCESSENTRY32 lppe  //指向進程入口結構
);
BOOL WINAPI Process32Next(
  HANDLE hSnapshot,        //這裏參數同Process32First
  LPPROCESSENTRY32 lppe  //同上
);

首先使用CreateToolhelp32Snapshot()創建系統快照句柄(hprocess是我們聲明用來保存創建的快照句柄):

hProcess=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
然後調用Process32First()獲得系統快照中的第一個進程信息(Report是BOOL型作爲判斷系統快照中下一條進程記錄):

 report=Process32First(hProcess,pinfo);
接着用一個循環調用來遍歷系統中所有運行的進程:

 while(report)
 {
 hModule=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,pinfo->th32ProcessID);
  Module32First(hModule, minfo); 
  GetShortPathName(minfo->szExePath,shortpath,256);
  printf("%s --- %s/n",pinfo->szExeFile,shortpath);
  report=Process32Next(hProcess, pinfo);   
 }

筆者曾通過對Pstools工具包裏的Pslist.exe反編譯,發現該工具用的就是這種方法。如果你查詢MSDN,可以找到一個比這個功能更加完善的源程序。


方法二

 第二種方法也很常見,通過MSDN就可以找到例子代碼,它是通過Psapi.dll提供的API函數EnumProcesses和EnumProcessModules來實現。有一點要說明的是,Visual Studio提供的SDK包裏沒有提供相應的Psapi.h和與之相對應的導入庫,筆者當時就很納悶,MSDN裏的例子居然編譯不通,後來才發現,編譯時根本找不到“psapi.h”。呵呵,還好,MSDN至少告訴我們他們都包含Psapi.dll裏,用VC自帶的Depend工具一查,果然。這樣就好辦了,我們可以自己找到這些函數入口地址。

小知識:C也好C++也好,一般的函數名本質上都是一個地址,在Win32的API裏,它是指向函數所在Dll模塊裏函數實現的入口地址。

下面是第二種方法的實現過程:首先,先把Psapi.dll裏要用到函數都定義好,方便後面顯示調用。

//在psaipi.dll中的函數EnumProcesses用來枚舉進程
typedef BOOL (_stdcall *ENUMPROCESSES)(  //注意這裏要指明調用約定爲-stdcall
 DWORD* pProcessIds,  //指向進程ID數組鏈 
 DWORD cb,    //ID數組的大小,用字節計數
 DWORD* pBytesReturned);   //返回的字節
//在psapi.dll中的函數EnumProcessModules用來枚舉進程模塊
typedef BOOL (_stdcall *ENUMPROCESSMODULES)(
 HANDLE hProcess,   //進程句柄
 HMODULE* lphModule, //指向模塊句柄數組鏈
 DWORD cb,    //模塊句柄數組大小,字節計數
 LPDWORD lpcbNeeded);   //存儲所有模塊句柄所需的字節數
//在psapi.dll中的函數GetModuleFileNameEx獲得進程模塊名
typedef DWORD (_stdcall *GETMODULEFILENAMEEX)(
 HANDLE hProcess,   //進程句柄
 HMODULE hModule,   //進程句柄
 LPTSTR lpFilename,   //存放模塊全路徑名
 DWORD nSize    //lpFilename緩衝區大小,字符計算
);

好,通過每個定義的函數前的註釋你可以清楚看到這些原函數名,然後當然要加載包含這些函數模塊Psapi.dll啦:
 hPsDll = LoadLibrary("PSAPI.DLL");
接着,通過Dll入口,我們查找每個函數地址:

pEnumProcesses  =
(ENUMPROCESSES)GetProcAddress(hPsDll, "EnumProcesses");
pEnumProcessModules  =
(ENUMPROCESSMODULES)GetProcAddress(hPsDll, "EnumProcessModules");
pGetModuleFileNameEx  =
   (GETMODULEFILENAMEEX)GetProcAddress(hPsDll, "GetModuleFileNameExA");
注意第三個函數名GetModuleFileNameExA,在Dll裏有以A和W結尾區分函數,A指採用的是ANSI字符串方式,W則是UNICODE方式。於是,我們可以用下面的語句枚舉進程:
pEnumProcesses(processid, sizeof(processid), &needed);
 processcount=needed/sizeof(DWORD);
 
 for (i=0;i<processcount;i++)
 {
  //打開進程
  hProcess=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ,
false, processid[i]);
  if (hProcess)
  {
   pEnumProcessModules(hProcess, &hModule, sizeof(hModule), &needed);
   pGetModuleFileNameEx(hProcess, hModule, path, sizeof(path));
   GetShortPathName(path,path,256);
   itoa(processid[i],temp,10);
   printf("%s --- %s/n",path,temp);
  }
  else
   printf("Failed!!!/n");
 }
 當然,在Google上一搜索,可以找到不少提供的Psapi.h頭文件和對應的Psapi.lib庫,這樣,你就可以更方便用這種方法了。

方法三

 也許你會說前兩種方法全世界都知道了,沒什麼大不了的!呵呵,那麼筆者現在介紹的第三種方法,你就未必知道了。本方法利用了Windows NT/2000下終端服務API函數WTSOpenServer()和WTSEnumerateProcess()來實現,這兩個函數都定義在Wtsapi32.dll裏。具體的關於終端服務方面的知識,大家可以查詢MSDN。
首先,我們來顯示申明這兩個函數原形:
typedef HANDLE (_stdcall *WTSOPENSERVER)(
LPTSTR pServerName //NetBios指定的終端服務名,如果我們查看本地終端所有進程信息我們可以通過在控制檯命令行下用nbtstat –an來獲取本機NetBios名。如圖3所示。
);  

 四種方法實現VC枚舉系統當前進程
 
圖3


typedef BOOL (_stdcall *WTSENUMERATEPROCESSES)(
  HANDLE hServer, //WTSOpenServer返回的句柄 
  DWORD Reserved,  //保留值, 0
  DWORD Version,   //指定枚舉要求的版本, 必須爲1
  PWTS_PROCESS_INFO* ppProcessInfo, //這個參數是關鍵,存放我們要的進程名和進程id
  DWORD* pCount   //用來存放ppProcessInfo裏WTS_PROCESS_INFO結構的數量指針
);

和前面一樣,要先裝載Wtsapi32.dll模塊,獲取關鍵函數地址:

hWtsApi32 = LoadLibrary("wtsapi32.dll");
pWtsOpenServer
  = (WTSOPENSERVER)GetProcAddress(hWtsApi32, "WTSOpenServerA");
pWtsEnumerateProcesses
= (WTSENUMERATEPROCESSES)GetProcAddress
(hWtsApi32,"WTSEnumerateProcessesA");

通過Argv[1]給終端服務名(這裏我們賦本機NetBios名)賦一個值並打開這項服務:

char* szServerName = argv[1];
hWtsServer = pWtsOpenServer(szServerName);

然後開始遍歷終端服務器上的所有進程,這裏我們是指本機的所有進程
if(!pWtsEnumerateProcesses(hWtsServer,
       0,
       1,
       &pWtspi,
       &dwCount))
 {
  printf("enum processes error: %d/n", GetLastError());
  return;
 };
 
 for(int i=0; i<dwCount; i++)
 {
  printf("ps_Id: %d/t/tps_name: %s/n", pWtspi[i].ProcessId, pWtspi[i].pProcessName);
 }

怎麼樣,酷吧,跟前面兩種方法效果一樣!


方法四

 最後筆者介紹一種最少人用的方法,本方法是從“幻影旅團”論壇上看到的。後面給出的源代碼也是從他們那拷貝過來的。呵呵,這種方法也提供給我們一種很好的思路。

 這第四種方法利用了Native Api的NtQuerySystemInformation函數來實現。同樣沒有該函數的導入庫,也要自己定義原形。整個實現不難,可是有點煩,因爲在該函數參數結構上,筆者通查MSDN查了很久才找到這些相關的結構,下面我們來看看這個方法的實現吧。
先是自定義函數原形:

typedef NTSTATUS (__stdcall *PZWQUERYSYSTEMINFORMATION)
                 (IN SYSTEM_INFORMATION_CLASS SystemInformationClass, 
                  IN OUT PVOID SystemInformation, 
                  IN ULONG SystemInformationLength, 
                  OUT PULONG ReturnLength);
然後,還是和前面一樣,要找到ZwQuerySystemInformation在Ntdll.dll模塊裏的入口地址:

hModule = GetModuleHandle((LPCTSTR)"ntdll.dll");
pZwQuerySystemInformation =(PZWQUERYSYSTEMINFORMATION)
GetProcAddress(hModule, (LPCTSTR)"ZwQuerySystemInformation");
獲得指向進程信息數組鏈的第一條進程信息:
pSystemProcessInformation = (SYSTEM_PROCESS_INFORMATION *)pvProcessList;

和方法一方法二一樣,在獲得第一條進程信息後,開始循環遍歷,列出其餘的進程:
while (TRUE)
    {
        if (pSystemProcessInformation->NextEntryDelta == 0) //如果是最後一條,則終止循環。
            break;                                   

        pSystemProcessInformation=(SYSTEM_PROCESS_INFORMATION*)((PCHAR)pSystemProcessInformation+ pSystemProcessInformation->NextEntryDelta);
        pProcessName = (char *)malloc(pSystemProcessInformation->ProcessName.Length + 2);
}

特別注意的是,這裏SYSTEM_PROCESS_INFORMATION結構裏進程名的類型爲UNICODE_STRING, 而UNICODE_STRING裏的成員Buffer定義的是Unicode類型。
typedef struct _SYSTEM_PROCESS_INFORMATION 

    DWORD NextEntryDelta; 
    DWORD dThreadCount; 
    DWORD dReserved01; 
    DWORD dReserved02; 
    DWORD dReserved03; 
    DWORD dReserved04; 
    DWORD dReserved05; 
    DWORD dReserved06; 
    FILETIME ftCreateTime; /* relative to 01-01-1601 */ 
    FILETIME ftUserTime; /* 100 nsec units */ 
    FILETIME ftKernelTime; /* 100 nsec units */ 
    UNICODE_STRING ProcessName;      //這就是進程名
    DWORD BasePriority; 
    DWORD dUniqueProcessId;            //進程ID
    DWORD dParentProcessID; 
    DWORD dHandleCount; 
    DWORD dReserved07; 
    DWORD dReserved08; 
    DWORD VmCounters; 
    DWORD dCommitCharge; 
    PVOID ThreadInfos[1];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;                 //注意,這裏爲Unicode類型
} UNICODE_STRING, *PUNICODE_STRING;

所以,最後我們要調用WideCharToMultiByte()來還原成ANSI字符串以便輸出:
WideCharToMultiByte(CP_ACP, 
                      0,
                    pSystemProcessInformation->ProcessName.Buffer,
                    pSystemProcessInformation->ProcessName.Length + 1,
                    pProcessName,
                    pSystemProcessInformation->ProcessName.Length + 1,
                    NULL,
                    NULL);
具體每個參數大家可以參照MSDN,很簡單的。以上四種方法的完整源代碼見光盤。

 從這裏看出,查看系統當前活動進程的實現也不過如此,況且,筆者還沒有給出第五種方法(可以通過WMI的COM接口函數CoCreateInstance(),ConnectServer(),ExecQuery()等函數來實現,留給讀者自己練習),呵呵,好玩吧?居然有那麼多種方法。

    寫這篇文章的目的是想告訴大家,有時候通過自己動手,會發現以前認爲很NB的東西原來也是很簡單的,只要擅於總結,常動手,我們自己也可以自己打造一些方便好用的小工具來,你說呢?!
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章