登录
首页 >  文章 >  软件教程

Win32进程句柄表与内核对象深度解析

时间:2025-05-28 08:18:27 272浏览 收藏

在Win32进程中,句柄表与内核对象的关系至关重要。句柄表通过索引允许三环程序访问位于高地址空间的内核对象,如EPROCESS结构。内核对象包括进程、线程、事件等多种类型,可通过CloseHandle API进行管理。多进程可以通过OpenProcess API共享内核对象,引用计数机制确保对象在无引用时被销毁。进程间还可以通过继承句柄技术共享资源,具体由安全属性结构体中的bInheritHandle参数决定。此外,进程ID(PID)是全局句柄表的索引,理解这些概念有助于掌握Windows程序开发中的进程操作和资源管理。

在不改变文章大意和图片位置的前提下,以下是经过伪原创处理的文章:


句柄表与内核对象之间的关系是什么?首先,我们需要了解什么是句柄表,什么是内核对象。

一、句柄表和内核对象的概念

  1. 句柄表的生成

当我们使用CreateProcess函数时,它会返回一个进程句柄和一个线程句柄。在调用CreateProcess时,内核会创建一个EPROCESS结构来保存进程信息。

win32进程概念之句柄表,以及内核对象.

然而,如何让三环程序使用这个EPROCESS呢?直接返回EPROCESS是不行的,因为EPROCESS位于高两G的地址空间,三环程序无法访问。为了解决这个问题,Windows创建了一个表格,并返回这个表格的索引。我们使用的就是这个索引。

  1. 什么是内核对象

内核对象就是我们提到的EPROCESS。内核对象有很多种,具体可以参考CloseHandle API,它可以关闭哪些内核对象:

  • Access token
  • Communications device
  • Console input
  • Console screen buffer
  • Event
  • File
  • File mapping
  • I/O completion port
  • Job
  • Mailslot
  • Memory resource notification
  • Mutex
  • Named pipe
  • Pipe
  • Process
  • Semaphore
  • Thread
  • Transaction
  • Waitable timer

这些对象可以操作事件、文件、互斥体、线程等。

二、多进程共享内核对象

  1. 第一种方法:使用OpenProcess

在Windows程序中,我们操作的都是内核对象。我们可以通过OpenProcess API来打开一个已有的进程的内核对象。

win32进程概念之句柄表,以及内核对象.

每个进程的句柄表都是私有的。例如,在第一张表中,句柄索引为1,对应的内核对象为A。如果将这个索引传给B进程,是没有用的。B进程只有在使用API打开后,才能获得A内核对象。

中间的紫色表代表引用计数。每次引用内核对象,这个值会加1。CloseHandle的作用是使内核对象的引用计数减1。如果所有引用都被关闭,那么内核对象将无人使用,也没有任何引用,因此会被销毁。也就是说,当内核对象的引用计数为0时,它才会被真正销毁。

线程是一个特例:当线程的内核对象引用计数为0时,它不会被关闭。此时,必须先关闭线程,然后使用CloseHandle使引用计数减1。

  1. 使用继承句柄技术

在Windows程序中,A进程创建B进程,或者带有内核对象的API在创建时,都有一个SD属性,即安全属性。这个属性可以表示你创建的句柄是否可以被继承。

例如,CreateEvent()创建事件。我们先不讨论API的作用,看看它的参数:

win32进程概念之句柄表,以及内核对象.

HANDLE CreateEventA(
    LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性结构体
    BOOL                  bManualReset,
    BOOL                  bInitialState,
    LPCSTR                lpName);

第一个参数是安全属性结构体。如果我们不指定,默认使用父进程的。

安全属性结构体如下:

win32进程概念之句柄表,以及内核对象.

typedef struct _SECURITY_ATTRIBUTES {
    DWORD  nLength;                         // 当前结构体大小,Windows扩展使用
    LPVOID lpSecurityDescriptor;            // 表明这个句柄给谁使用,谁可以访问,谁可以关闭。不重要,具体可以查看API中的结构体定义
    BOOL   bInheritHandle;                  // 重要,表明句柄是否可以被继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

win32进程概念之句柄表,以及内核对象.

如果我们的句柄可以被继承,那么句柄表的第一项就会填1,表示这个句柄可以被继承。如果不能继承,则为0。

此时,子进程就可以继承父进程的所有可继承的句柄表。注意,是所有可继承的句柄,可以共享,如下图所示:

win32进程概念之句柄表,以及内核对象.

A进程创建的B和D是可以继承的,因此子进程可以完全复制A进程的可继承句柄表。不允许继承的句柄则赋值为0,如下图所示:

win32进程概念之句柄表,以及内核对象.

三、进程PID解析

在Windows任务管理器中,有PID选项,我们可以选择查看。而且在Windows中也经常听到进程ID的概念。

那么,进程ID到底是什么?

其实,进程ID是全局句柄表的一个索引。上面提到的句柄表都是私有的句柄表,而PID是全局句柄表中的。

这个全局句柄表记录了所有正在运行的进程的句柄,而且是唯一的。如果进程死亡,这个PID可能会指向其他句柄,但也是唯一的,如下图所示:

win32进程概念之句柄表,以及内核对象.

这个全局句柄表才是真正有意义的。为什么这么说呢?

我们可以做个测试:

  1. 使用OpenProcess打开进程句柄。
  2. 使用TerminateProcess结束进程。
OpenProcess(访问权限, 句柄是否可以继承, 进程PID)
TerminateProcess(进程句柄, 自定义的退出码)

使用这两个API可以测试我们已有的进程是否可以被关闭。如果测试后你会发现,只有通过PID获得的句柄才是有用的,也就是说,全局句柄表才是关键,而上面提到的都是子进程的句柄表。

四、常用进程操作API

  1. GetModuleFileName():获取当前模块路径,例如:c:\1.exe
  2. GetCurrentDirectory():获取当前的工作目录,例如:c:\text\abc
  3. OpenProcess():根据进程PID打开进程,获取进程句柄。
  4. FindWindow():根据类名和文件名,返回窗口句柄。
  5. GetWindowThreadProcessId():根据窗口句柄,获取进程PID。
  6. EnumProcesses:遍历所有进程,返回进程PID。具体参考MSDN,有提供的例子。
  7. GetCommandLine():获取命令行参数。
  8. CreateToolhelp32Snapshot():创建进程快照。如果你了解逆向工程,你会知道FS寄存器中的TEB和PEB结构中存储了当前模块或进程的链表。这是保存当前时刻的快照。我们可以进行遍历,具体参考MSDN或本博客。

五、编写Windows程序遇到的问题

在编写Windows程序时,我们会包含windows.h,但有些函数可能没有。例如,我们提到的第八个函数,快照函数。

此时,我们需要查询MSDN。我们可以在网页上搜索一下:

win32进程概念之句柄表,以及内核对象.

我们可以在下方看到所需的头文件是tlhelp32.h,此时我们包含它即可。

遇到的问题2:

有时候我们包含了头文件并使用了,但调用API时出错。为什么?

原因是有些API在高版本中才有,低版本中使用时没有导出。此时使用会出错,提示没有这个API。

解决方法:如果你学过Win32,你会理解这个方法。如果没有学过也没关系。这种问题很少遇到,博主也只遇到过一次。

可以使用LoadLibrary加载所需的DLL,然后使用GetProcAddress获取函数地址,使用函数指针来调用这个函数。

以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。

相关阅读
更多>
最新阅读
更多>
课程推荐
更多>