🍃基本 概念介绍
'本书的作者是Jeffrey,他从最基本的角度拆解计算机系统的基本构件。如今的计算机过于庞大,我也更欣赏这种首先从最底层开始,深刻理解基本构件,一旦掌握最基本的东西,就很容易向自己的知识库中逐步加入更多高层的东西。所以,理解最基本的系统始终是我们最应该花时间去掌握的技能,母亲常说没有一个骨架,身上是挂不住几两肉的。希望我和现在读这段文字的你一起走过这一趟最有价值,也是困难重重的旅程,你的关注是这趟旅程最好的助力
1. 何为操作系统内核?
操作系统也无非就是一个程序,与其他程序不同的是他是所有其他程序依赖的“大哥”,也可以说是杀马特家族的**“杀马特团长”,这个老大来负责其他程序的“饮食起居”**,大哥给他们资源,给他们名誉、地位,带他们出来拍视频。每一个进程都被虚拟化,以为自己独有一个好大哥。
2. 何为内核对象?
大家都用过各种各样面向对象的编程语言,所谓核心对象,便是操作系统内核所维护的一个数据结构。他们也有自己的方法,有属于自己的变量。例如:访问令牌(access token)对象、事件对象、文件对象、mutex 对象、pipe 对象、进程对象、semaphore对象、线程对象…
每个内核对象都只是一个内存块,它由内核分配,并 只能由内核访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。
3. 应用程序如何操纵这些内核对象呢?
答案是利用 Windows 提供的一组函数,以经过良好定义的方式来操纵这些结构。使用这些函数,始终可以访问这些内核对象。
调用一个会创建内核对象的函数后,函数会返回一个句柄(handle),它标识了创建的对象。所谓就是个把手,用这个把手去控制一个内核对象。
可以将这个句柄想象为一个不透明(opaque)的值,它可由进程中的任何线程使用。
在 32 位 Windows 进程中,句柄是一个32 位值;在 64 位 Windows 进程中,则是一个 64 位值。
可将这个句柄传给各种 Windows函数,告诉系统你想操纵哪一个内核对象。
为了增强操作系统的可靠性,这些句柄值是与进程相关的。所以,如果将句柄值传给另一个进程中的线程(通过某种进程间通信方式),那么另一个进程用你的进程的句柄值来发出调用时,就可能失败;甚至更糟,它们会在你的进程句柄表的同一个索引位置处,创建到一个完全不同的内核对象的引用。
内核知道当前有多少个进程正在使用一个特定的内核对象,因为每个对象都包含一个使用计数(usage count)。使用计数是所有内核对象类型都有的一个数据成员。
内核对象的安全性:内核可以用一个安全描述符(SD)来保护。安全描述符描述了谁(通常是它的创建者)拥有对象;哪些用户和用户允许访问或使用此对象;以及哪些组和用户拒绝访问此对象。
安全描述符通常在编写服务器应用程序的时候使用。
用于创建内核对象的所有函数几乎都有指向一个 SECURITY_ATTRIBUTES 结构的指针作
为参数,如下面的 CreateFileMapping 函数所示
HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName);
大多数应用程序只是为这个参数传入 NULL。
SECURITY_ATTRIBUTES 结构如下所示:
typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES;
4. 除了内核对象还有什么对象
除了使用内核对象,应用程序可能还要使用其他类型的对象,比如菜单、窗口、鼠标光标、 画刷和字体。这些属于 User 对象或
GDI(Graphical Device Interface)对象,而非内核对象。 首次进行 Windows 编程时,往往很难区分
User/GDI 对象和内核对象。例如,图标是 User 对象还是内核对象?要想判断一个对象是不是内核对象,最简单的方式是查看创建这个对象
的函数。几乎所有创建内核对象的函数都有一个允许你指定安全属性信息的参数,就像前面 展示的 CreateFileMapping 函数一样。
相反,用于创建 User 或 GDI 对象的函数都没有 PSECURITY_ATTRIBUTES 参数。例如下 面的 CreateIcon
函数:
HICON CreateIcon( HINSTANCE hinst, int nWidth, int nHeight, BYTE cPlanes, BYTE cBitsPixel, CONST BYTE *pbANDbits, CONST BYTE *pbXORbits);
5. 进程内核对象句柄表
一个进程在初始化时,系统将为它分配一个句柄表(handle table)。这个句柄表仅供内核对象使用,不适用于 User 或 GDI 对象。
句柄表:
6. 创建一个内核对象
一个进程首次初始化的时候,其句柄表为空。当进程内的一个线程调用一个会创建内核对象的函数时(比如 CreateFileMapping),内核将为这个对象分配并初始化一个内存块。
然后,内核扫描进程的句柄表,查找一个空白的记录项(empty entry)。并对其进行初始化。具体地说,指针成员会被设置成内核对象的数据结构的内部内存地址,访问掩码将被设置成拥有完全访问
权限,标志也会设置。
下面列出了一些用创建内核对象的函数:
HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, size_t dwStackSize, LPTHREAD_START_ROUTINE pfnStartAddress, PVOID pvParam, DWORD dwCreationFlags, PDWORD pdwThreadId); HANDLE CreateFile( PCTSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_ATTRIBUTES psa, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName);
如果传入一个无效的句柄,函数就会失败, GetLastError 会返回 6 (ERROR_INVALID_HANDLE)。由于句柄值实际是作为进程句柄表的索引来使用的,所
以这些句柄是相对于当前这个进程的,无法供其他进程使用。如果你真的在其他进程中使用它,那么实际引用的是那个进程的句柄表的同一个索引位置处的内核对象——只是索引值相同而已,你根本不知道它会指向什么对象。
调用函数来创建一个内核对象时,如果调用失败,那么返回的句柄值通常为 0(NULL),这
就是为什么第一个有效的句柄值为 4 的原因。之所以失败,可能是由于系统内存不足,或者遇到了一个安全问题。遗憾的是,有几个函数在调用失败时会返回句柄值–1(也就是在WinBase.h 中定义的 INVALID_HANDLE_VALUE)。例如,如果 CreateFile 无法打开指定文件,它会返回 INVALID_HANDLE_VALUE,而不是 NULL。凡是用于创建内核对象的
函数,在你检查它们的返回的值时,务必相当仔细。具体说,以当前这个例子为例,只有在
调用 CreateFile 时,才准许将它的返回值与 INVALID_HANDLE_VALUE 比较。以下代码是
不正确的:
HANDLE hMutex = CreateMutex(…); if (hMutex == INVALID_HANDLE_VALUE) { // 这里的代码永远不会执行, // 因为 CreateMutex 在失败时总是返回 NULL。 } 类似地,以下代码也是不正确的: HANDLE hFile = CreateFile(…); if (hFile == NULL) { // 这里的代码永远不会执行,因为 CreateFile // 在失败的时候会返回 INVALID_HANDLE_VALUE(-1)。 }
7. 关闭内核对象
无论以什么方式创建内核对象,都要调用 CloseHandle 向系统指出你已经结束使用对象,如下所示:
cpp BOOL CloseHandle(HANDLE hobject);
在内部,该函数首先检查主调进程的句柄表,验证“传给函数的句柄值”标识的是“进程确实有权访问的一个对象”。如果句柄是有效的,系统就将获得内核对象的数据结构的地址,并在结构中递减“使用计数”成员。如果使用计数变成 0,内核对象将被销毁,并从内存中删除。 如果传给 CloseHandle函数的是一个无效的句柄,那么可能发生以下两种情况之一:如果进程是正常运行的, CloseHandle 将返回 FALSE , 而 GetLastError 返 回ERROR_INVALID_HANDLE。如果进程正在被调试,那么系统将抛出0xC0000008异常(“指定了无效的句柄”),便于你调试这个错误。
当你的应用程序运行时,它可能会泄漏内核对象;但当进程终止运行,系统能保证一切都被正确清除。顺便说一下,这适用于所有内核对象、资源(包括 GDI 对象在内)以及内存块。
如果“句柄数”列显示的数字持续增长,下一步就是判断哪些内核对象尚未关闭。为此,可
以使用 Sysinternals 提供的一款免费 Process Explorer 工具(网址http://www.microsoft.com/technet/sysinternals/ProcessesAndThreads/ProcessExplorer.mspx)。首
先,右击下方 Handlers 窗格的标题行(如果这个窗格没有出现,请从 View 菜单中选择 Show
Lower Pane),并从弹出菜单中选择 Select Columns。然后,在图 3-3 所示的“Select Columns”
对话框中,勾选择所有列标题:
在此期间生成的每个新的内核对象都显示为绿色
第一列显示了没有关闭的内核对象的类型。为了使你有更大的机会确定泄漏位置,
第二列提供了内核对象的名称。利用作为内核对象名称的字符串,你可以在不同的进程之间共享这个对象。显然,根据第一列的类型和第二列的名称,你可以更容易地判
断出哪个对象没有关闭。如果泄漏了大量对象,它们并不一定会被命名,因为只能创建一个命名对象的一个实例——其他尝试会单纯地打开那个实例。
8.跨进程边界共享内核对象
在很多时候,不同进程中运行的线程需要共享内核对象。下面罗列了一些理由。
利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据块。
借助 mailslots 和 named pipes,在网络中的不同计算机上运行的进程可以相互发送 数据块。
mutexes、semaphores 和事件允许不同进程中的线程同步执行。例如,一个应用程序可能需要在完成某个任务之后,向另一个应用程序发出通知。
由于内核对象的句柄是相对于每一个进程的,所以执行这些任务并不轻松。不过,Microsoft也有充分的理由需要将句柄设计成“相对于进程”(process-relative)的。其中最重要的原因是健壮性(可靠性)。如果把内核对象句柄设计成相对于整个系统,或者说把它们设计成**“系统级”的句柄**,一个进程就可以很容易获得到“另一个进程正在使用的一个对象”的句柄,从而对该进程造成严重破坏。之所以将句柄设计成“相对于进程”,或者说把它们设计成“进程级”的句柄,还有一个原因是安全性。内核对象是用安全性保护起来的,一个进程在试图操纵一个对象之前,必须先申请操纵它的权限。对象的创建者为了阻止一个未经许可的用户“碰”自己的对象,只需拒绝该用户访问它。
在下一节,我们要讨论如何利用三种不同的机制来允许进程共享内核对象:使用对象句柄继承;为对象命名;以及复制对象句柄:
1.使用对象句柄继承
只有在进程之间有一个父–子关系的时候,才可以使用对象句柄继承。在这种情况下,父进程有一个或多个内核对象句柄可以使用,而且父进程决定生成一个子进程,并允许子进程访问父进程的内核对象。为了使这种继承生效,父进程必须执行几个步骤。
首先,当父进程创建一个内核对象时,父进程必须向系统指出它希望这个对象的句柄是可以继承的。我有时听到别人说起“对象继承”这个词。但是,世界上根本没有“对象继承”这样的事情。Windows支持的是“对象句柄的继承”;换言之,只有句柄才是可以继承的,对象本身是不能继承的。
为了创建一个可继承的句柄,父进程必须分配并初始化一个SECURITY_ATTRIBUTES
结构,并将这个结构的地址传给具体的 Create 函数。以下代码创建了一个 mutex 对象,返回它的一个可继承的句柄:
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // 使返回的句柄成为可继承的句柄 HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
以上代码初始化了一个 SECURITY_ATTRIBUTES
结构,表明对象要用默认安全性来创建,而且返回的句柄应该是可继承的 下一步是由父进程生成子进程。这是通过 CreateProcess 函数来完成的,如下所示: 接下来谈谈在进程的句柄表记录项中保存的标志。句柄表中的每个记录项都有一个指明句柄是否可以继承的标志位。如果在创建内核对象的时候将
NULL 作 为 PSECURITY_ATTRIBUTES 参数传入,则返回的句柄是不可继承的,这个标志位为 0。将bInheritHandle 成员设为 TRUE,则导致这个标志位被设为 1。 以表 3-2 的进程句柄表为例。在这个例子中,进程有权访问两个内核对象(句柄 1 和 3)。
句柄 1 是不可继承的,但句柄 3 是可以继承的。 表 3-2 包含两个有效记录项的进程句柄表
内核对象的内容被保存在内核地址空间中——系统上运行的所有进程都共享这个空间。对于 32 位系统,这是 0x80000000 到 0xFFFFFFFF 之间的内存空间。对于 64 位系统,则是 0x00000400’00000000 到 0xFFFFFFF’FFFFFFFF 之间的内存空间。访问掩码与父进程中的一样,标志也是一样的。这意味着假如子进程用
CreateProcess 来生成它自己的子进程(其父的孙进程),那么在将 bInheritHandles 参数设为 TRUE
的前提下,孙进程也会继承这个内核对象句柄。在孙进程的句柄表中,继承的对象句柄将具有相同的句柄值,相同的访问掩码,以及相同的标志。内核对象的使用计数将再次递增。
BOOL CreateProcess( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES psaThread, BOOL bInheritHandles, DWORD dwCreationFlags, PVOID pvEnvironment, PCTSTR pszCurrentDirectory, LPSTARTUPINFO pStartupInfo, PPROCESS_INFORMATION pProcessInformation);
2. 改变句柄的标志
挖坑后补
3. 为对象命名
HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateEvent( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, BOOL bInitialState, PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName); HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName); HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);
所有这些函数的最后一个参数都是 pszName。向此参数传入 NULL,相当于向系统表明你
要创建一个未命名的(即匿名)内核对象。如果创建的是一个无名对象,可以利用上一节讨 论过的继承技术,或者利用下一节即将讨论的
DuplicateHandle 函数来实现进程间的对象共 享。如果要根据对象名称来共享一个对象,你必须为此对象指定一个名称。
9. Terminal Services 命名空间
挖坑后补
10. private 命名空间
挖坑后补
11. 复制对象句柄
挖坑后补
🍑实现过程
进程由两个组件构成:
一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地
方。
一个地址空间,其中包含所有执行体(executable)或DLL模块的代码和数据。此外,
它还包含动态内存分配,比如线程堆栈和堆的分配
编写第一个windows应用程序
Windows支持两种类型的应用程序:GUI程序和CUI程序。
#include <ntddk.h> VOID DriverUnload(PDRIVER_OBJECT driver) { DbgPrint("HW: Our driver is unloading...\r\n"); } NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) { DbgPrint("HW: Hello, World!\r\n"); driver->DriverUnload = DriverUnload; return STATUS_SUCCESS; }