【Windows 核心编程】Windows 核心编程 -- 内核对象
一,对象和句柄
1)对象:是静态定义的对象类型的单个运行时实例。对象类型包括系统定义的数据类型,在数据类型实例上的操作的函数以及一组对象属性集。
对象为完成下列四种重要的操作系统任务提供了方便的方法:
为系统资源提供可读的名字;
在进程间共享资源和数据;
保护资源以免非授权访问;
引用跟踪,它允许系统确知对象什么时候不再使用以便自动被释放;(引用计数,稍后讲到)
2)在Windows API中我们常常调用CreateXXX() 函数来建立一个对象,函数返回一个句柄(HANDLE),我们通过保存这个返回的句柄,在以后的程序中通过该句柄对建立的对象执行一些操作。
在Windows操作系统中我们常常接触的有三种对象类型:
Windows内核对象 (事件对象,文件对象,进程对象,I/O完成端口对象,互斥量对象,进程对象,线程对象等等):由执行体(Excutive)对象管理器(Object Manager)管理,内核对象结构体保存在系统内存空间(0x80000000-0xFFFFFFFF),句柄值与进程相关。
Windows GDI对象 (画笔对象,画刷对象等):由Windows子系统管理,句柄值在系统,会话范围 (system-wide / session-wide) 有效。
Windows USER对象 (窗口对象,菜单对象等) :由Windows子系统管理,句柄值在系统,会话范围 (system-wide / session-wide) 有效。
3)句柄,是整个windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个四字节长的数值(32位系统),来标志应用程序中的不同对象和同类对象中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是一个指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不用在I/O文件中,它是毫无用处的。
句柄是windows用来标志应用程序中建立的或使用的唯一整数,windows使用了大量的句柄来标志很多对象。
HINSTANCE hInstance;
可以改成:
HANDLE hInstance;
二,内核对象
1)内核对象:每个内核对象都只是一个内存块,它由操作系统分配,并只能由操作系统访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。
少数成员(安全描述符和使用计数等)是所有对象都有的,但其它大多数成员都是不同类型的对象特有的。
注意:由于内核对象的数据结构只能由操作系统内核访问,所以应用程序不能在内存中定位这些数据结构并直接更改其内容。 应用程序只能间接的操纵这些数据结构,这就有了句柄(HANDL)。调用一个创建内核对象的函数后系统会返回一个句柄,它标识了多创建的对象,它可由进程中的任何线程访问。
2)使用计数
我们应该搞清楚的是:内核对象的所有者是操作系统,而内核对象的句柄的所有者是进程。
操作系统使用一个叫使用计数的数据成员来记录内核对象的使用情况。每当有进程取得该内核对象的访问后,使用计数加1,当进程终止或关闭该内核对象句柄时,使用计数减1;操作系统记录每个内核对象的使用计数,一旦内核对象的使用计数为0,操作系统销毁该对象。
内核对象的生命期可能长于创建它的那个进程。 进程创建一个内核对象,如果该内核对象使用了跨进程的内核对象共享,即其它进程也取得了该内核对象的使用权限。
3)内核对象安全性
内核对象可以用一个安全描述符(security descriptor,SD)来保护。它描述了谁拥有对象,哪些用户和用户被允许访问或使用此对象;哪些组和用户被拒绝访问此对象。通常在编写服务器应用程序的时候使用。
用于创建内核对象的函数几乎都有指向一个SECURITY_ATTRIBUTES 结构的指针作为参数,如下面的CreateFileMapping :
HANDLE CreateFileMapping( //创建一个新的文件映射内核对象
HANDLE hFile, //物理文件句柄
PSECURITY_ATTRIBUTES psa, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
PCTSTR pszName); //共享内存名称
大多数应用程序只是为这个参数传入NULL,这样创建的内核对象具有默认的安全性——具体包括哪些默认的安全性要取决于当前进程的安全令牌(security token)。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle; //是否允许继承
} SECURITY_ATTRIBUTES;
用法:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor=pSD;
sa.bInheritHandle=FALSE
HANDLE hFileMapping = CreateFileMapping(INVALIDE_HANDLE_VALUE,&sa,PAGE_READWRITE,0,1024,TEXT("MyFileMapping"));
如果已经存在句柄
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ,FALSE,TEXT("MyFileMapping")); //打开文件映射对象,进行访问
三,进程内核对象句柄表
一个进程在初始化时,系统将为它分配一个句柄表(handle table)。这个句柄表仅供内核对象使用 ,不适用于USER对象或GDI对象。
而对于句柄表的结构我们不得而知,不过大概如下:
索引
指向内核对象内存块的指针
1
0x????????
0x????????
0x????????
2
0x????????
0x????????
0x????????
1)创建一个内核对象
一个进程首次初始化时,其句柄表为空。当进程的某个线程调用了一个会创建内核对象的函数时,内核将为这个对象分配内存,并扫描进程句柄表,找到一个空白的记录项,并对其进行初始化。指针成员被设置成内核对象的数据结构的内部内存地址,访问掩码被设置成拥有完全访问权限,标志也会被设置,根据句柄的继承性。
由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前所使用的进程相关的,无法供其它进程使用。如果我们真的在其它进程使用它,那么实际引用的知识那个进程的句柄表中位于同一个索引的内核对象——只是索引值相同而已,我们根本不知道它会指向什么对象。
调用函数 来创建一个内核对象时,如果调用失败,那么返回的句柄值通常为0(NULL),有几个函数调用失败时返回的是-1,即INVALID_HANDLE_VALUE;所以在检查函数返回值时务必相当仔细。
例子:创建内核对象的函数
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
HANDLE CreateFile(
LPCTSTR lpFileName, //指向文件名的指针
DWORD dwDesiredAccess, //访问模式(写/读)
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全属性的指针
DWORD dwCreationDisposition, //如何创建
DWORD dwFlagsAndAttributes, //文件属性
HANDLE hTemplateFile //用于复制文件句柄 );
注意:只有调用CreateFile时,才能将其返回值与ERROR_INVLID_HANDLE 比较 不要与NULL比较。其他的CreateMutex()不要与之比较
2)关闭内核对象
我们通过调用函数:
BOOL CloseHandle(HANDLE hobject);
来表明结束使用内核对象,该函数将清除进程句柄表中对应的记录项,并且递减该内核对象的“使用计数” ,如果该内核对象的"使用计数"减为“0”,那么操作系统销毁该内核对象,即将对象从内存中清除,但是“使用计数”大于“0”,该内核对象将继续保留,因为还有其它进程拥有该内核对象的访问权限。
通常,在创建一个内核对象时,我们会将相应句柄保存到一个变量中。将此变量作为参数调用CloseHandle()函数后,最好能同时将这个变量设为NULL。
在不需要再使用对象时,释放相应对象句柄是一个好习惯,如果句柄没有及时释放,可能引起内存泄露。
当进程终止运行,操作系统会确保此进程所使用的所有资源都被释放。进程终止时,系统扫描进程句柄表,操作系统将关闭所有句柄表中的有效记录项。只要这些对象中有一个的“使用计数”减为0,内核就会销毁对象。
进程终止时,系统也将保证进程拥有的所有资源(GDI对象等)以及内存块都将得到正确清除。
Process Explorer
三,跨进程共享内核对像
不同进程运行中的线程处于某些原因需要共享内核对象:
利用文件映射对象,可以再同一台机器上运行的两个不同进程之间共享数据块;
借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送数据块;
互斥量,信号量和事件允许不同进程中的线程同步执行。
有三种不同机制来允许进程共享对河对象:使用对象句柄继承;为对象命名;复制对象句柄。
1)使用对象句柄继承
还记得 DUPLICATE_CLOSE_SOURCE :会关闭源进程中的句柄,内核对象的“使用计数”不会受到影响。