【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)使用对象句柄继承

还记得SECURITY_ATTRIBUTES结构吗?SECURITY_ATTRIBUTES结构中有个BOOL bInheritHandle属性,该属性就是制定该内核对象是否具有继承性。

对象句柄继承,就是说让进程的子进程继承父进程的对象句柄,实例如下:

  • 首先建立一个可继承的对象句柄;

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; // 指定返回的句柄具有继承性;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

  • 父进程生成子进程,并让子进程继承对象句柄值;

BOOL CreateProcess(

PCTSTR pszApplicationName,

PTSTR pszCommandLine,

PSECURITY_ATTRIBUTES psaProcess,

PSECURITY_ATTRIBUTES psaThread,

BOOL bInheritHandles,//制定子进程是否继承父进程句柄表中“可继承的句柄”,设为“TRUE”

DWORD dwCreationFlags,

PVOID pvEnvironment,

PCTSTR pszCurrentDirectory,

LPSTARTUPINFO pStartupInfo,

PPROCESS_INFORMATION pProcessInformation);

这样创建的子进程就继承了父进程句柄表中“可继承的句柄”,并且在子进程句柄表中的位置和父进程句柄中的位置是一样的。即在父进程和子进程中,对一个内核对象的句柄值是完全一样的,在句柄表中的记录项也是一样的。

为了使子进程得到它想要的一个内核对象的句柄值,最常见的方式是将句柄值作为命令行参数传给子进程

改变句柄的标志

父进程创建一个内核对象,得到一个可继承的句柄,然后生成了两个子进程。但是父进程只希望其中的一个子进程继承内核对象的句柄。控制那些子进程能继承内核对象的句柄。

改变对象句柄表项信息:

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask, //想更改哪些标志
DWORD dwFlags); //希望把标志设置成什么
dwMask:
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002 //不允许关闭句柄

打开一个句柄的继承

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT ,HANDLE_FLAG_INHERIT);

关闭一个句柄的继承

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT ,0);

如果

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);

CloseHandle(hObj); //返回错误,因为已经设置不允许关闭句柄

改正后则允许关闭句柄

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,0);

CloseHandle(hObj);//返回错误,因为已经设置不允许关闭句柄

检查一个句柄是否可以继承,获取对象句柄表项信息:
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags);


四,为对象命名

许多(但不是全部)内核对象都可以进行命名。举例:
HANDLE CreateMutex(

PSECURITY_ATTRIBUTES psa,

BOOL bInitialOwner,

PCTSTR pszName); //如果传入 NULL 相当于创建一个匿名的内核对象
要为内核对象制定一个名称,参数pszName应该传入一个“以0为终止符的名称字符串”的地址。所有这些对象都共享同一个命名空间,及时它们的类型并不相同。

相较于使用句柄继承,利用对象的名称来共享内核对象,最大的一个优势是“进程B不一定是进程A的子进程”。

当要建立指定名称的对象时,系统先检查是否存在此“名称”的对象,如果确实存在内核将接着检查对象的类型,如果类型匹配,再进行一个安全检查,验证调用者是否拥有对该对象的安全访问权限。如果答案是肯定的,系统将在调用进程句柄表中建立以记录项,并将其初始化为指向现有的内核对象。如果对象的类型不匹配或调用者被拒绝访问,函数返回NULL。
HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// Opened a handle to an existing object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are ignored.
} else {
// Created a brand new object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are used to construct the object.
}
HANDLE OpenMutex(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

PCTSTR pszName);

Create*函数和Open*函数的主要区别是,如果对象存在,Create*函数会创建它;Open*函数则不同,如果对象不存在,它只是简单地以调用失败而告终。

但是不同内核对象即使类型不同,名称也不能相同,如

HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("text"));

HANDLE hSem = CreateSempaphore(NULL,1,1,TEXT("text")); //返回空(类型不匹配)

DWORD dwErrorCode = GetLastError();// 返回6 ERROR_INVALID_HANDLE

正确的继承应该如下

HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,TEXT("text"));

HANDLE hMutexProcessB=CreateMutex(NULL,FALSE,TEXT("text"));

DWORD dwErrorCode = GetLastError();// 返回 ERROR_ALREADY_EXISTS

为了实现内核对象的共享,还可以不比新建句柄,而选择打开句柄如

HANDLE OpenMutex(DWORD dwDesiredAccess,BOOL bInheritHandle,PCTSTR pszName);

五,复制对象句柄
使用函数DuplicateHandle实现共享内核对象:

获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本。
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, //内核对象句柄
HANDLE hSourceHandle, //指向任何类型的内核对象的一个句柄,与上面那个进程相关
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle, //handle变量的地址
DWORD dwDesiredAccess, //
BOOL bInheritHandle,
DWORD dwOptions);
dwOptions:
DUPLICATE_SAME_ACCESS:表明我们希望目标句柄拥有与源进程的句柄一样的访问掩码。
DUPLICATE_CLOSE_SOURCE:会关闭源进程中的句柄,内核对象的“使用计数”不会受到影响。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值