Windows 是一个基于对象的操作系统,它公开了各种类型的对象(通常称为 askernelObjects),它们提供了 Windows 中的大部分功能。示例对象类型是进程、线程和文件。
内核对象
Windows内核公开了各种类型的对象,供用户模式进程、内核本身和内核模式驱动程序使用。这些类型的实例是系统(内核)空间的数据结构,当用户或内核模式代码要求这样做时,由对象管理器(Executive的一部分)创建和管理。内核对象是引用计数的,所以只有当对象的最后一个引用被释放时,该对象才会被销毁并从内存中释放。
有相当多的对象类型被Windows内核支持。要想看一看,可以运行Sysinternals(elevated)中的WinObjtool,找到ObjectTypes目录。图2-1显示了它的样子。这些类型可以根据它们的可见性和用途进行分类。
通过Windows API导出到用户模式的类型。例如:mutex、semaphore、文件、进程、线程、定时器。本书讨论了这些对象类型中的许多类型。 -没有输出到用户模式的类型,但在Windows DriverKit(WDK)中被记录下来,供设备驱动编写者使用。例如:device、driver、callback.-甚至在WDK中都没有记录的类型(至少在写作的时候)。这些对象类型只供内核本身使用。例如:分区、关键事件、核心消息。
由于内核对象驻留在系统空间中,因此不能从用户模式直接访问它们。应用程序必须使用间接机制来访问内核对象,称为句柄。句柄至少提供以下好处:
- 在未来的Windows版本中,对象类型的数据结构的任何变化都不会影响任何客端
- 对对象的访问可以通过安全访问检查来控制。
- 手柄对进程来说是私有的,所以在一个进程中拥有一个特定对象的手柄在另一个进程的上下文中毫无意义。
引用计数
内核对象是引用计数的。对象管理器维护一个句柄计数和一个指针计数,它们的总和就是一个对象的总引用计数(直接指针可以从内核模式获得)。一旦用户模式客户端使用的对象不再需要,客户端代码应该通过调用CloseHandle关闭用于访问该对象的句柄。从那时起,代码应该认为这个句柄是无效的。试图通过已关闭的句柄访问该对象将失败,GetLastError返回ERROR_INVALID_HANDLE(6)。在一般情况下,客户端不知道该对象是否已经被销毁。如果对象的引用下降到零,对象管理器将删除该对象。
句柄值为 4 的倍数,其中第一个有效句柄为 4;零永远不是有效的句柄值。64位系统也一样
从逻辑上讲,句柄是一个索引,它指向一个句柄表中的条目数组,这些条目数组以进程为单位进行维护,逻辑上指向驻留在系统空间中的内核对象。有各种Create和Open函数来创建/打开对象和检索这些对象的句柄。如果对象不能被创建或打开,返回的句柄在大多数情况下是NULL(0)。这个规则的一个明显的例外是CreateFile函数,如果它失败了,会返回INVALID_HANDLE_VALUE(-1)
如果函数成功并且提供了一个名字,返回的句柄可以是一个新的mutex,也可以是一个有这个名字的现有mutex。代码可以通过调用GetLastError并将结果与ERROR_ALREADY_EXISTS相比较来检查。如果是,那么它就不是一个新的对象,而是一个现有对象的另一个句柄。这是一种罕见的情况,即使有关的API成功了,也可以调用GetLastError。
运行单实例进程
一个相当著名的ERROR_ALREADY_EXIST案例的用法是限制一个可执行文件只有一个进程实例。通常情况下,如果你在资源管理器中双击一个可执行文件,一个新的进程就会基于该可执行文件产生。如果你重复这个操作,就会在同一个可执行文件的基础上创建另一个进程。如果你想阻止第二个进程的启动,或者至少让它在检测到有相同可执行文件的另一个进程实例已经运行时关闭,你会怎么做?
诀窍是使用一些命名的内核对象(通常使用mutex,尽管可以使用任何命名的对象类型),在那里创建一个具有特定名称的对象。如果该对象已经存在,一定有另一个实例已经在运行,所以进程可以关闭(可能通知其兄弟姐妹这一事实)。它是一个用WTL构建的基于对话框的应用程序。图2-3显示了这个应用程序的运行情况。如果你尝试启动这个应用程序的更多实例,你会发现第一个窗口会记录来自新进程实例的信息,然后退出。
在WinMain函数中,我们首先创建mutex。如果失败了,那就是出了大问题,我们就放弃。
HANDLE hMutex = ::CreateMutex(nullptr,FALSE,L"SingleinstanceMutex");
if(!hMutex){
CString text;
text.Format(L"Failed to create mutex(Error: %d)",::GetLastError());
::MessageBox(nullptr, text,L"Single instance ",MB_OK);
return 0;
}
创建mutex的失败应该是极其罕见的。最有可能失败的情况是,另一个具有相同名称的内核对象(这不是一个突变体)已经存在。现在我们得到了一个突变体的适当句柄,唯一的问题是突变体是否真的被创建了,还是我们收到了另一个现有突变体的句柄(估计是由这个可执行程序的前一个实例创建的)。
if(::GetlastError() != ERROR_ALREADY_EXISTS){
NotifyOtherInstances();
return 0;
}
如果对象在调用CreateMutex之前已经存在,那么我们就调用一个辅助函数,向现有的实例发送一些信息并退出。这里是NotifyOtherInstance。
#define WM_NOTIFY_INSTANCE (WM_USER + 100)
void NotifyOtherInstance(){
auto hWnd=::FindWindow(nullptr,L"Single Instance");
if(!hWnd) {
::MessageBox(nullptr,L"Failed to locate other instance window",
L"Single Instance", MB_OK);
return;
}
::PostMessage(hWnd, WM_NOTIFY_INSTANCE,::GetCurrentProcessId(),0);
::ShowWindow(hWnd, SW_NORMAL);
::SetForegroundWindow(hWnd);
return 0;
}
该函数用FindWindow函数搜索现有的窗口,并使用窗口的标题作为搜索标准。这在一般情况下并不理想,但对于这个例子来说已经足够了。
一旦找到了窗口,我们就向该窗口发送一条自定义消息,并将当前进程的ID作为一个参数。这显示在对话框的列表框中。
最后一步是通过对话框处理 WM_NOTIFY_INSTANCE 消息。在 WTL 中,窗口消息使用宏映射到函数。 MainDlg.hi中的对话框类(CMainDlg)的消息映射在这里:
BEGIN_MESSAGE_MAP(CMainDlg)
MESSAGE_HANDLER(WM_NOTIFY_INSTANCE,OnNotifyInstance)
MESSAGE_HANDLER(WM_INITDIALOG,OnInitDialog)
COMMAND_ID_HANDLER(IDCANCEL,OnCancel)
END_MSG_MAP()
自定义消息被映射到OnNotifyInstancemember函数,像这样实现。
LRESULT CMainDlg::OnNotifInstance(UINT,WPARM,wParam, LPARAM BOOL &){
CString text;
text.Format(L"Message from another instance (PID: %d)",wParam);
AddText(text);
return 0;
}
进程ID从wParamparameter中提取,一些文本通过AddTexthelper函数被添加到列表框中。
void CMainDlg::AddText(PCWSTR text){
CTime dt = CTime::GetCurrentTime();
m_List.AddString(dt.Format(L"%T")+L": "+text);
}
m_List 是CListBox 类型,一种Windows列表窗口装饰
句柄
一个句柄间接地指向一个位于内核的,很小的结构体,该结构体包含了一些关于句柄的信息。以下是32位和64 位系统的句柄结构体
在 32 位系统上,这个句柄条目的大小为 8 个字节,而在 64 位系统上它的大小为 16 个字节(技术上 12 个字节就足够了,但为了对齐目的,它扩展到了 16 个字节)。每个条目都有以下部分
- 一个指向目标内容的指针,(由于较低的位被用于标记和通过地址对齐来改善CPU的访问时间,一个对象的地址在32位系统上是8的倍数,在64位系统上是16的倍数。)
- 访问mask,表示这个句柄可以做什么,换句话说,访问mask 是句柄的功能权限
- 三个标志 :继承,保护不被关闭 和 关闭时检查
访问掩码是一个位掩码,每个 “1 “位表示可以使用该句柄进行某种操作。访问掩码在通过创建一个对象或打开一个现有对象来创建句柄时被设置。如果对象被创建,那么调用者通常有对该对象的完全访问权。但是如果对象被打开,调用者需要指定所需的访问掩码,它可能得到也可能得不到。
例如,如果应用程序想要终止某个进程,它必须首先调用 OpenProcess 函数,以获取所需进程的句柄,访问掩码为(至少为)PROCESS_TERMINATE,否则无法使用该句柄终止进程。如果调用成功,那么对TerminateProcess 的调用必然会成功。下面是一个给定进程ID 终止进程的例子:
bool KillProcess(DWORD pid){
HANDLE hProcess = ::OpenProcess(PROCESS_TERMINATE,FALSE,pid);
if (!hProcess)
return false;
BOOL success = ::TerminateProcess(hProcess,1);
::CloseHandle(hProcess);
return success!=FALSE;
}
OpenProcess 函数原型如下:
HANDLE OpenProcess(
_In_ DWORD dwDesiredAccess,//访问mask
_In_ BOOL bInheritHandle,//flag
_In_ DWORD dwProcessID,)
由于这是一个开放操作,有关的对象已经存在,客户需要指定它需要什么访问掩码来访问该对象。一个访问掩码有两种类型的访问位:通用和特定。在上面的例子中,一个进程的特定访问位是PROCESS_TERMINATE。其他位包括PROCESS_QUERY_INFORMATION、PROCESS_VM_OPERATION等。请参考OpenProces的文档以找到完整的列表。
客户端代码应该使用什么访问掩码?通常,它应该反映客户端代码打算对对象执行的操作。要求多于需要可能会失败,要求少显然不够好
与每个句柄关联的标志如下:
- 继承 - 此标志用于句柄继承 - 一种允许在协作进程之间共享对象的机制。
- 关闭时审计 - 该标志指示当句柄关闭时是否应该在安全日志中写入审计条目。此标志很少使用,默认情况下处于关闭状态。
- 防止关闭 - 设置此标志可防止手柄被关闭。调用CloseHandle 将返回FALSE 和GetLastError 返回ERROR_INVALID_HANLLE(6)。如果该进程在调试器下运行,则会引发异常并显示以下消息:“0xC0000235:在已通过 NtSetInformationObject 保护不受关闭的句柄上调用了 NtClose”。这个标志很少有用
改变继承和保护标志可以通过SetHandleInformation函数来完成,它是这样定义的。:
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
BOOLSetHandleInformation(
_In_ HANDLE hObject,
_In_ DWORD dwMask,
_In_ DWORD dwFlags);
第一个参数是句柄本身。第二个参数是一个位掩码,指示要操作的标志。最后一个参数是这些标志的实际值。例如,要在某个句柄上设置“防止关闭”位,可以使用以下代码:
::SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE,
HANDLE_FLAG_PROTECT_FROM_CLOSE);
以下代码片段删除了相同的位:
::SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE,0);
也存在相反的函数来读回这些标志。
BOOLGetHandleInformation(_In_ HANDLE hObject,
_Out_ LPDWORD lpdwFlags);
可以使用Sysinternals中的process Explorer工具查看从特定进程打开的句柄.默认情况下显示的列仅为typeandname。我通过右键单击标题区域并单击选择列添加了以下列:句柄、对象地址、访问和解码访问。
- 句柄 - 这是句柄值本身,仅与此过程相关。相同的句柄值可以有不同的含义,即 - 指向不同的对象,或者甚至可能是空索引。•
- Type - 对象类型名称。比如 WinObj 中的对象类型目录。
- 对象地址-这是实际对象结构所在的内核地址。请注意,这些地址以64位的零十六进制数字结尾(在32位系统上,地址以“8”或“0”结尾)。没有包含此信息的用户模式代码,但它可用于调试目的:如果一个对象有两个句柄,并且您想知道它们是否指向同一个对象,则可以比较对象第2章:对象和句柄地址;如果它们是一样的,那就是同一个物体。否则,句柄将指向不同的对象。
- 访问 - 这是上面讨论的访问掩码。要解释存储在这个十六进制值中的位,你需要在文档中找到访问掩码位。为了缓解这个问题,请使用解码的访问权限(Decoded Accesscolumn)。
- 解码的访问权限—为常见的对象类型提供了一个访问掩码位的字符串表示。这使得解释访问掩码位更容易,而不需要深入研究文档。
默认情况下,Process Explorer的句柄视图仅显示命名对象的句柄。要查看所有句柄,请从查看菜单启用显示未命名句柄和映射选项
术语 “名称 “比它看起来更棘手。进程探索认为命名的对象不一定是实际的名字,但在某些情况下是方便的称呼。例如,图2-5中显示了进程和线程句柄,尽管进程和线程不能有基于字符串的名称。还有一些对象类型的 “名称 “并不是它们的名字;最令人困惑的是文件和钥匙。我们将在本章后面的 “对象名称 “一节中讨论这种 “奇怪的现象”。一个进程的句柄表中的句柄总数可以作为ProcessExplorer和Task Manager中的一个列。图2-7显示了添加到任务管理器的这一列。