关于服务和进程 - 第一部分
如何以编程方式控制 Windows NT 中的进程和服务
引言
本教程适用于使用 VC++ 6 以编程方式控制 Windows 2000 平台上的进程。本教程可能包含至少三部分。下一部分将在每篇教程的末尾提及。撰写本教程时,我使用的是 Windows 2000 Professional 和 VC++ 6,但移植到其他 Windows 或 VC 版本应该不需要太多更改,尽管我尚未对此进行测试。
本教程虽然详尽,但并非快速修补的去处。它旨在使您能够学习如何在与服务相关的进程中进行处理。本教程的出现源于互联网上对此类材料的缺乏,尤其是在进程方面。反馈和建议应通过电子邮件发送给我:r_thampi@hotmail.com。请使用相关的邮件主题行,以便将您的邮件与垃圾邮件区分开来。
您可以分发本教程,条件是您不得更改、编辑或删除教程中的任何内容,包括但不限于作者署名,并且不得将本教程出售以谋取个人利益(无论是金钱还是其他)。
本教程中的示例不遵循传统的 Windows 编程模型。我将项目保持为一个简单的控制台.exe。这样做是为了集中精力处理预期任务,而不是为了说明 Windows 编程模型,后者会包含更多的代码。
Outline
- 获取所有服务及其 PID。
- 你需要什么
- 涉及的函数,包括其工作原理的简要描述
- Process.cpp
- Process.cpp 剖析
- 第二部分。
- 附录
A. 获取所有服务及其 PID
1:- 您需要什么
为了更轻松地处理进程,有一个辅助库。在本教程中,我使用 Microsoft 提供的 PSAPI(进程状态应用程序编程接口)库来解释进程。相关文件是Psapi.h、Psapi.lib 和Psapi.dll。
Psapi.h 和Psapi.lib:这些文件包含示例中将讨论的所有函数。这些文件必须包含在代码中并在项目中引用。您可以从 http://www.microsoft.com/downloads/details.aspx?familyid=A55B6B43-E24F-4EA3-A93E-40C0EC4F68E5&displaylang=en 下载 Windows 200 的安装程序。(如果文件位置已更改,您可能需要搜索该安装程序。)
运行安装程序文件时,它会提供安装项目所需之外的更多内容。要仅安装所需内容,请选择自定义并安装Windows Core SDK。
Paspi.dll:编译后,Psapi.h 和Psapi.lib 文件将尝试引用Psapi.dll。因此,这是使您的exe正常运行的必要文件,因此请务必在分发您的项目之前将其包含在内。不幸的是,此 DLL 通常不随 Windows 一起提供,但随Windows Core SDK安装程序一起提供,但万一您只需要此 DLL,您可以使用以下链接下载它
http://www.microsoft.com/downloads/release.asp?releaseid=30337
读者应理解基础 C 并且熟悉 VC。如果您擅长 C 编程,但在 VC 中设置项目有问题,请参阅附录 B。
2:- 涉及的函数,包括其工作原理的简要描述
在开始项目之前,了解以下函数是有益的。这些函数是EnumProcesses
、OpenProcess
和GetModuleBaseName
。如果您不能立即理解它们也没关系,因为它们在代码中也有详细的解释。
EnumProcesses
:- EnumProcesses
函数检索系统中每个进程的进程标识符(PID
)。
BOOL EnumProcesses(DWORD* pProcessIds, DWORD cb, DWORD* pBytesReturned);
参数
pProcessIds
:[out] 这是我们定义的数组的指针。该函数将在此数组中填充进程标识符列表。cb
:[in] 这是一个DWORD
,包含pProcessIDs
数组的大小(以字节为单位)。pBytesReturned
:[out] 这是 Windows 将用于存储返回到pProcessIds
数组中的字节数的DWORD
指针。
正如您所见,一旦您调用此函数,您就获得了列出您计算机系统上运行的进程所需的所有数据!EnumProcesses
的返回值为零(表示没有错误)。
OpenProcess
:- 您需要理解的下一个函数是OpenProcess
,顾名思义,该函数为您提供进程句柄。它需要三个参数。
HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
参数
dwDesiredAccess
:[in] 此DWORD
应包含您希望对该进程拥有的所需(一个或多个)访问权限,并会与进程的安全描述符进行交叉检查。如果调用者启用了SeDebugPrivilege
特权,则无论安全描述符的内容如何,都会授予请求的访问权限。有关访问权限列表,请参阅附录 A。bInheritHandle
:[in] 如果此变量中的值为TRUE
,则创建的进程将继承句柄,否则进程将不继承此句柄。当我们将进行到本教程的第三部分时,您将理解这意味着什么。dwProcessId
:[in] 此变量应包含要打开的进程的标识符,换句话说,就是进程的PID
。
如果函数成功,则返回值是指定进程的打开句柄;如果失败,则返回值为 NULL
。
GetModuleBaseName
:- GetModuleBaseName
函数检索指定模块的基础名称。本教程的第二部分将深入探讨“模块”。现在,您只需要知道许多模块构成了进程。
DWORD GetModuleBaseName(HANDLE hProcess, HMODULE hModule, LPTSTR lpBaseName, DWORD nSize);
参数
hProcess
:[in] 包含模块的进程的句柄在此变量中可用。要使用此函数,句柄必须具有PROCESS_QUERY_INFORMATION
和PROCESS_VM_READ
访问权限。(有关它们代表的含义,请参阅附录 A。)hModule
:[in] 此参数应包含模块的句柄。如果此参数为NULL
,则此函数返回用于创建调用进程的文件名。lpBaseName
:[out] 此变量为您提供指向接收模块基础名称的缓冲区的指针。如果基础名称比nSize
参数指定的字符数长,则基础名称将被截断。nSize
:[in] 此参数应包含lpBaseName
缓冲区的大小(以字符为单位)。
如果函数成功,则返回值指定复制到缓冲区中的字符串的长度(以字符为单位);如果函数失败,则返回值为零。
3:- Process.cpp
include <windows.h> #include <stdio.h> #include <tchar.h> #include <psapi.h> void main() { DWORD ProcessesIDs[50], cbNeeded, cProcesses; unsigned int i; // The default of <unknown> is given so that if GetModuleBaseName does not return // the base name of the module then <unknown> will be printed instead of the base name. TCHAR szProcessName[50] = TEXT("<unknown>"); // if Enumprocess returns zero (fails) then quit the program. if ( !EnumProcesses( ProcessesIDs, sizeof(ProcessesIDs), &cbNeeded ) ) return; // Calculate how many process identifiers were returned. cProcesses = cbNeeded / sizeof(DWORD); // This for loop will be enumerating each process. for ( i = 0; i < cProcesses; i++ ) { // Get a handle to the process. The process to which the handle will be returned // will depend on the variable i. HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, ProcessesIDs[i] ); // Get the process name. if (NULL != hProcess ) { GetModuleBaseName( hProcess, NULL, szProcessName, sizeof(szProcessName)/sizeof(TCHAR) ); } // Print the process name and identifier. _tprintf( TEXT("Processname = %s, PID = %u \n\n"), szProcessName, ProcessesIDs[i]); //Every handle is to be closed after its use is over. CloseHandle( hProcess ); //End of for loop. } }
4:- Process.cpp 剖析
上面的代码可以复制到 VC++ 6 中进行编译,但请记住链接包含文件Psapi.h和库Psapi.lib。有关详细信息,请参阅附录 B。
现在让我们尝试理解代码本身。首先,您声明了几个变量。前三个是ProcessesIDs[50]
、cbNeeded
和cProcesses
,它们的类型都是DWORD
。
第一个ProcessIDs
是DWORD
类型的数组。它用于存储 PID。如果您系统的进程超过 50 个,则需要增加此数组的大小。
第二个是cbNeeded
,用于存储返回到ProcessIDs
数组中的字节数。
第三个是cProcesses
。它用于获取系统中运行的进程数。
最后一个变量是szProcessName[50]
(i
变量仅用于for
循环)。此变量用于存储模块名称。如果您运行的进程名称超过 50 个 TCHAR
,则可以相应地增加数组的大小。
现在,在声明之后,让我们继续进行语句。
if ( !EnumProcesses( ProcessesIDs, sizeof(ProcessesIDs), &cbNeeded ) ) return;
EnumProcesses
函数在失败时返回零,在成功时返回非零值,因此您可以安全地使用 if
语句进行检查。如果函数失败,您可以停止进一步执行。
您可以看到 EnumProcess
有三个参数。在这三个参数中,唯一为函数提供值的参数是第二个参数,它表示第一个参数变量的大小。在这种情况下,第一个参数变量(ProcessIDs
)是一个包含 50 个元素的 DWORD
数组。由于 DWORD
是 4 个字节,因此 sizeof(ProcessesIDs)
的返回值将是 4*50 = 200
字节。
ProcessIDs
参数变量中包含什么并不重要。函数返回时,ProcessesIDs
是一个指向数组的指针,该数组将包含 PID
列表。函数执行后,您可以遍历 ProcessesIDs
数组并从每个元素中读取每个 PID
。
第三个参数是指向 DWORD
的指针。函数返回函数返回到 ProcessIDs
数组的实际字节数,因此函数运行后,您将在 cbNeeded
中获得 ProcessIDs
中包含所需数据的字节数。
例如,假设有 15 个 PID。当 EnumProcesses
运行时,它将返回 15 个 PID
到 ProcessIDs
数组中,并且 cbNeeded
表明只有 15 个元素被填充。它通过返回 15 * 4 = 60 字节来实现,其中 4 是一个元素(以字节为单位的 DWORD
)的大小。
现在,我们有了系统中运行的 PID
数组(存储在 ProcessesIDs
中),我们可以进入下一步。但在继续之前,如果您存储返回的 PID
数量,可以让事情变得更容易。因此,添加一行
cProcesses = cbNeeded / sizeof(DWORD);
上面的代码获取 DWORD
的大小(4 字节),并除以 cbNeeded
的值(其中包含返回到数组 ProcessIDs
的总字节数)。这将给出存储在 ProcessIDs
中的 PID
总数,并存储在 cProcesses
中。
现在您需要一个循环来遍历数组 ProcessesIDs
中的每个 PID
。为此,您循环 cProcesses
次。进入循环后,您需要 ProcessesIDs[i]
元素中 PID
的句柄。这正是 OpenProcess
函数的用武之地。
HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, ProcessesIDs[i] );
OpenProcess
函数也需要三个参数。第一个参数指示您希望对该进程拥有的访问类型。有关访问权限及其含义的列表,请参阅附录 A。第二个参数是 BOOL
值。如果此值为 TRUE
,则此进程创建的进程将继承句柄。由于您不需要创建其他进程,因此可以将其保留为 FALSE
。最后一个参数是要获取句柄的 PID
。
一旦您输入所有这些输入并调用函数,该函数将返回进程的句柄,该句柄存储在名为hProcess
的变量中,并进行检查以确保hProcess
不为NULL
。
现在来看获取进程名称的最后一个函数!
GetModuleBaseName( hProcess, NULL, szProcessName, sizeof(szProcessName)/sizeof(TCHAR) );
此函数有四个参数,其中三个您需要提供数据,它返回一个参数。第一个参数提供进程的句柄,这是 OpenProcess
的返回值。您只需要在此处使用 hProcess
。
对于第二个参数(与模块有关,在本教程的第二部分中将详细介绍),目前只需输入 NULL
。
对于第四个参数,请提供 szProcessName
的大小,该大小与它可以容纳的字符数有关。请记住,返回的内容超过数组大小的部分将被截断。为此,请获取数组的总大小(以字节为单位),然后除以 TCHAR
的大小。
第三个参数是要从函数返回的数组。它将包含进程的名称。搞定……
在 for
循环结束之前,您将使用此行打印进程名称和 PID
_tprintf( TEXT("Processname = %s, PID = %u \n\n"), szProcessName, ProcessesIDs[i]);
使用 OpenProcess
关闭句柄,如下所示
CloseHandle( hProcess );
B.第二部分
本教程的第二部分将详细介绍进程,并深入探讨进程中的模块。
C.附录
附录 A
访问权限可以是
进程名称 | 描述 |
PROCESS_ALL_ACCESS |
进程对象的所有可能访问权限 |
PROCESS_CREATE_PROCESS |
创建进程所需 |
PROCESS_CREATE_THREAD |
创建线程所需 |
PROCESS_DUP_HANDLE |
使用 DuplicateHandle 复制句柄所需 |
PROCESS_QUERY_INFORMATION |
检索进程的某些信息(例如其令牌、退出代码和优先级类)所需 |
PROCESS_QUERY_LIMITED_INFORMATION |
检索进程的某些信息所需 |
PROCESS_SET_QUOTA |
使用 SetProcessWorkingSetSize 设置内存限制所需 |
PROCESS_SET_INFORMATION |
设置进程的某些信息(例如其优先级类)所需 |
PROCESS_SUSPEND_RESUME |
挂起或恢复进程所需 |
PROCESS_TERMINATE |
使用 TerminateProcess 终止进程所需 |
PROCESS_VM_OPERATION |
对进程的地址空间执行操作所需 |
PROCESS_VM_READ |
使用 ReadProcessMemory 读取进程内存所需 |
PROCESS_VM_WRITE |
使用 WriteProcessMemory 向进程内存写入所需 |
SYNCHRONIZE |
使用 wait 函数等待进程终止所需 |
附录B
设置项目所用的 VC。
首先确定您的系统是否已包含以下文件:Psapi.h、Psapi.lib 和Psapi.dll。如果已有,请转到本附录的“设置 VC”部分。
如果没有,请从以下链接下载 NT 的 PSAPI SDK 可再发行组件:http://www.microsoft.com/downloads/details.aspx?familyid=A55B6B43-E24F-4EA3-A93E-40C0EC4F68E5&displaylang=en。如果网站已移动或暂时不可用,您可能需要搜索 SDK。备用网站是 MSDN 或 Microsoft 主页。
安装此软件包,并记下其安装目录。例如,如果它安装在C:\Program Files\,则在Program Files目录中,您会找到另一个名为Microsoft Platform SDK的目录。此文件夹又会有两个名为Lib和Include的子文件夹,分别包含Psapi.lib和psapi.h。psapi.dll将在C:\WINNT\system32中找到。
设置 VC
打开 VC,然后转到文件 | 新建 | 项目 | Win32 控制台应用程序。输入项目名称,然后单击确定。
然后单击文件 | 新建 | 文件 | C++ 源文件。输入文件名,然后单击确定。C++ 源文件将打开,您可以在其中编写代码。
- 将本文档中的代码粘贴到您刚刚创建的 C++ 源文件中。
- 单击工具 | 选项 | 目录。在“显示目录用于”下选择“包含文件”。
- 双击“目录”标签下的空路径。(此空间看起来像一个文本区域)。双击后,您将看到一个省略号。
- 单击省略号,然后浏览到C:\Program Files\Microsoft Platform SDK\Include(或 Microsoft Platform SDK& 中Include文件夹的路径)。
- 点击**确定**。
- 在“显示目录用于”下选择“库文件”,然后按照前面的步骤指向C:\Program Files\Microsoft Platform SDK\Lib。
- 单击确定。
- 在 VC 主窗口的左侧,“文件视图”选项卡下,右键单击“资源文件”,然后单击“将文件添加到文件夹”。
- 在打开的浏览器中,将其指向Psapi.lib。
- 单击“确定”
- 按 F5 进行构建
- 转到命令提示符,然后导航到项目位置。
- 执行exe文件,您将看到系统中所有进程的
PID
和名称。